Check whether your feed is valid. For more information about JSON Feed, see the specification. Find the validator source code on GitHub.
GET validation response in JSON format.
{ "version": "https://jsonfeed.org/version/1.1", "title": "Engineer — Portfölj och anteckningar", "home_page_url": "https://engineer.company/sv/", "feed_url": "https://engineer.company/sv/feed.json", "description": "Engineer ApS — mjukvaruutveckling & IT-rådgivning i Köpenhamn, Danmark. En portfölj av dokumenterade ingenjörsresultat inom mjukvara, data, moln och IT.", "language": "sv", "icon": "https://engineer.company/assets/images/brand/card.webp", "favicon": "https://engineer.company/assets/icons/apple/apple-touch-icon.png", "authors": [ { "name": "Engineer ApS", "url": "https://engineer.company/sv/" } ], "items": [ { "id": "https://engineer.company/sv/portfolio/built-the-visual-identity-from-two-drawings-164/", "url": "https://engineer.company/sv/portfolio/built-the-visual-identity-from-two-drawings-164/", "title": "Byggde företagets ikon- och favicon‑uppsättningar från två handgjorda teckningar, där varje härledd resurs genereras om med skript och en kontroll fäller när en härledd fil har committats före sin källa.", "summary": "En enda teckning ger nu upphov till varje publicerad ikon, flikikonen inräknad, omgenerering är ett kommando, och en föråldrad fil får bygget att fallera.", "content_html": "<p><strong>Situation.</strong> Ett litet företags visuella identitet brukar köpas, genereras eller sättas ihop av arkivbilder, och resultatet är en identitet som inte tillhör någon. Det alternativa problemet är värre: handgjorda illustrationer som bara finns som exporterade filer, där originalteckningen har gått förlorad, exporterna redigeras direkt, och där det inom ett år finns versioner i bruk som inte stämmer överens med varandra och inget original som kan avgöra saken.</p>\n<p><strong>Uppgift.</strong> Identiteten måste ritas i stället för att anskaffas, och kedjan från teckning till publicerad fil måste vara reproducerbar, så att varje fil på webbplatsen går att härleda i stället för att förvaras.</p>\n<p><strong>Handling.</strong> De genererade illustrationerna började med två handgjorda teckningar: företagets märke, en rasterteckning, och ett kugghjul i vektor, ritat för webbläsarfliken med argumentet att en kraftig siluett håller vid 16 pixlar. Visade sida vid sida i 16, 32 och 48 pixlar mot ljusa och mörka flikrader förblev märket igenkännbart medan kugghjulet blev otydligt, så flikikonen gick över till märket, och varje ikon som webbplatsen publicerar framställs nu ur den enda teckningen. Ur den framställer skript varje härledd form: favicon‑uppsättningen i de storlekar som krävs, touch‑ikonerna och startbilderna i varje enhetsstorlek. Genereringen är en task som vem som helst kan köra, så frågan ”var kom den här filen ifrån” har ett svar som är ett kommando. Kontrollen av föråldrade filer är den del som får det att hålla: den jämför commitdatumet för varje härledd fil med dess källa och fallerar när en härledd fil committades först, vilket fångar exakt det fel som den här designen finns för att förhindra — någon redigerar källan, glömmer att generera om, och den publicerade webbplatsen fortsätter att visa illustrationer som inte längre stämmer med originalet. Regeln att härledda filer aldrig redigeras för hand står angiven där källorna ligger.</p>\n<p><strong>Resultat.</strong> En enda teckning ger nu upphov till varje publicerad ikon, flikikonen inräknad, omgenerering är ett kommando, och en föråldrad fil får bygget att fallera. Avvägningen är ett hårt beroende av verktygskedjan: de härledda filerna committas så att webbplatsen kan byggas var som helst, men att ändra identiteten kräver att genereringsverktygen fortfarande fungerar, och ett källformat som slutar gå att läsa tar med sig hela identiteten.</p>\n", "date_published": "2026-10-18T10:08:26+02:00", "date_modified": "2026-10-18T10:08:26+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Test och kvalitetssäkring", "UX/UI‑design", "Varumärke och marknadsföring", "Webbutveckling", "UI/UX‑design och designsystem", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/", "url": "https://engineer.company/sv/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/", "title": "Ökade varumärkets popularitet 200 gånger genom framgångsrik varumärkesutveckling på offline- och onlineplattformar.", "summary": "Ökade varumärkets räckvidd ungefär 200 gånger på plattformar offline och online och gjorde en okänd nykomling till ett välkänt namn.", "content_html": "<p><strong>Situation.</strong> Engineer ApS var en helt ny konsultfirma med verkligt tekniskt djup och nästan ingen som hade hört talas om den. Det är en särskild sorts frustration: kompetensen finns, arbetet skulle bli bra, men inget av det spelar någon roll om de kunder och partner som skulle vilja ha det inte vet att firman finns. En ung firma måste synas innan den kan vinna något.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att bygga ett varumärke som folk faktiskt skulle känna igen — både offline och online — och att omsätta firmans tekniska trovärdighet i en synlig närvaro på marknaden, i stället för att låta den förbli en väl bevarad hemlighet.</p>\n<p><strong>Handling.</strong> Varumärket byggdes medvetet och hölls konsekvent. Det började med en tydlig identitet — en röst, ett visuellt språk och en portfölj som lyfte fram konkreta tekniska resultat i stället för det vaga pratet om att ”leverera värde” som alla andra använder. Sedan fördes det ut i de kanaler som spelar roll för en firma av det här slaget — webbplatsen, LinkedIn, GitHub, fysiska evenemang — där varje kontaktpunkt sa samma sak i stället för att glida iväg åt sitt eget håll. Den röda tråden var att gå före med verkliga kundcase och verkliga resultat, så att trovärdigheten blev något man kunde se belägg för, inte bara ett påstående.</p>\n<p><strong>Resultat.</strong> Varumärkets räckvidd växte ungefär 200 gånger på plattformarna offline och online — en okänd nykomling blev något som folk faktiskt kände igen. Och det var ingen räckvidd för syns skull; synligheten började ge ett jämnt flöde av inkommande samtal och möjligheter som helt enkelt inte hade funnits tidigare.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Intressenter och rapportering", "Produkt och krav", "Varumärke och marknadsföring", "Produktstrategi och krav", "Varumärke, marknadsföring och SEO" ] }, { "id": "https://engineer.company/sv/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/", "url": "https://engineer.company/sv/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/", "title": "Ökade varumärkesengagemanget och varumärkeslojaliteten med 100 % genom analys av marknadstrender och insikter i konsumentbeteende.", "summary": "Fördubblade engagemang och lojalitet kring varumärket (+100 %) med analys av marknadstrender och insikt i konsumentbeteende och vann en återkommande publik.", "content_html": "<p><strong>Situation.</strong> När synligheten steg nådde Engineer ApS fler människor — men engagemanget var tunt. Folk lade märke till firman och gick vidare; det tidiga intresset blev inte till relationer som höll. Räckvidd utan engagemang är bara brus, och varumärket skapade mer brus än kontakter.</p>\n<p><strong>Uppgift.</strong> Målet var att fördjupa engagemanget och lojaliteten, och att göra det genom att faktiskt titta på vad marknaden och publiken reagerade på i stället för att lita på magkänslan.</p>\n<p><strong>Handling.</strong> Varumärket blev därför datainformerat i stället för intuitionsstyrt. Det innebar att titta på marknadstrenderna och på hur publiken faktiskt betedde sig i kanalerna — inte vad någon antog att de skulle gilla, utan vad de bevisligen engagerade sig i. På så sätt framträdde de ämnen och format som drog verklig uppmärksamhet, och innehållet och det uppsökande arbetet styrdes mot dem. Det viktiga var att strama åt återkopplingsslingan: se vad som gick hem, publicera mer i samma form nästa gång och låta varje varv bli lite bättre riktat än det förra.</p>\n<p><strong>Resultat.</strong> Engagemanget och lojaliteten fördubblades — en förbättring på 100 % — med en publik som kom tillbaka och engagerade sig i stället för att titta en gång och försvinna, och med märkbart starkare relationer till potentiella kunder och partner. Varumärket slutade sända ut i tomma intet och började bygga något som gav tillbaka.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dataanalys", "Intressenter och rapportering", "Produkt och krav", "Varumärke och marknadsföring", "Dataanalys och BI‑dashboards", "Produktstrategi och krav", "Varumärke, marknadsföring och SEO" ] }, { "id": "https://engineer.company/sv/portfolio/designed-a-comprehensive-infrastructure-framework-for-a-research-institute-3/", "url": "https://engineer.company/sv/portfolio/designed-a-comprehensive-infrastructure-framework-for-a-research-institute-3/", "title": "Utformade ett heltäckande infrastrukturramverk för ett forskningsinstitut som berörde 14 avdelningar, med flexibla moduler, enhetliga datapipelines och strukturerade supportstrategier för ett långsiktigt införande.", "summary": "Utformade ett heltäckande infrastrukturramverk för ett forskningsinstitut med 14 avdelningar — modulärt, med enhetliga datapipelines och supportstrategier.", "content_html": "<p><strong>Situation.</strong> Vid ett forskningsinstitut med 14 olika forskargrupper arbetade varje grupp med skilda datakällor, format, skalor och programvaruverktyg. Den tekniska kompetensen och de tillgängliga IT‑resurserna varierade kraftigt mellan grupperna. Ett fåtal hade lyckats ta fram och driftsätta egna IT‑lösningar, men många kämpade med komplexiteten i sina behov av datainfrastruktur, vilket tog värdefull tid och fokus från själva forskningen.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att ta fram en lösning som lät forskarna fokusera på sitt vetenskapliga arbete i stället för på IT‑utmaningar. Målet var att utforma och implementera en skalbar datainfrastruktur för hela institutet som kunde tillgodose de breda och skiftande kraven hos majoriteten av forskargrupperna.</p>\n<p><strong>Handling.</strong> En robust och framåtblickande infrastrukturplan togs fram som balanserade flexibilitet mot standardisering. Planen beskrev nyckelkomponenter som en modulär arkitektur, integrationsvägar för olika datakällor, användarvänliga gränssnitt anpassade efter olika tekniska kunskapsnivåer samt skalbara lösningar för lagring och bearbetning. Den innehöll också strategier för onboarding, support och styrning för att säkerställa att lösningen togs i bruk och höll över tid.</p>\n<p><strong>Resultat.</strong> Den färdiga infrastrukturplanen var både tekniskt sund och strategiskt i linje med institutets forskningsmål. Den gav organisationen en gemensam vision för datahantering, en tydlig väg mot en lättare IT‑börda för forskarna och en grund för en gemensam, effektiv och framtidssäker miljö för forskningsdata. Planen togs väl emot för sin inkluderande hållning, sin tydlighet och sin anpassningsbarhet, och den stakade ut en stark kurs för omställningen av institutets datainfrastruktur.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Datapipelines (ETL/ELT)", "Datastyrning", "Dokumentation", "Infrastruktur", "Intressenter och rapportering", "Lösningsarkitektur", "Plattformsarkitektur", "Tekniskt ledarskap", "Datastyrning och datakvalitet", "Plattforms- och lösningsarkitektur", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/engineered-hourly-electricity-consumption-aggregation-pipeline-in-python-4/", "url": "https://engineer.company/sv/portfolio/engineered-hourly-electricity-consumption-aggregation-pipeline-in-python-4/", "title": "Byggde en pipeline som aggregerar elförbrukning per timme i Python / SQL / Bash + Jq och nådde 180 ms för datamängder på 30 dagar från heterogena JSONL‑källor.", "summary": "Byggde en pipeline för elförbrukning i Python/SQL/Bash som aggregerar data för 30 dagar på 180 ms från heterogena JSONL-källor.", "content_html": "<p><strong>Situation.</strong> Företaget bearbetar stora volymer heterogena eldata från flera datakällor, var och en med sin egen rapporteringsfrekvens — från timintervall till 15‑minutersintervall och i vissa fall oregelbundna tidsstämplar. Variationen gör det svårt att skapa ett sammanhängande och jämförbart dataset. För att möjliggöra korrekt energianalys måste dessa data normaliseras till konsekventa förbrukningsvärden per timme, aggregerade per zon.</p>\n<p><strong>Uppgift.</strong> Målet var att ta rå händelsedata från ett historiskt dataset (i JSONL‑format) och omvandla dem till elförbrukningssiffror justerade till hela timmar, strukturerade som en rad per timme och zon. Konkret innebar uppgiften att:</p>\n<ul>\n<li>justera tidsstämplade data till strikta timintervall,</li>\n<li>aggregera den totala elproduktionen inom varje intervall genom att summera värdena i produktionsmixen,</li>\n<li>ta med gränsöverskridande utbyte genom att lägga till import och dra av export,</li>\n<li>leverera slutvärdena i ett strukturerat, skalbart format som lämpar sig för vidare analys.</li>\n<li>Lösningen behövde dessutom vara tillräckligt effektiv för att skala till långa tidsfönster (över 30 dagar) och till flera länder/zoner.</li>\n</ul>\n<p><strong>Handling.</strong> För att lösa uppgiften implementerades tre olika varianter av lösningen, i Python, JQ (för JSON‑bearbetning på kommandoraden) och SQL, var och en optimerad för olika sammanhang:</p>\n<p>Python valdes för sin flexibilitet, sin smidiga datahantering och sin förmåga att effektivt hantera transformationer i minnet. Med rena Python‑funktioner byggdes en pipeline som:</p>\n<ul>\n<li>tolkade JSONL‑filerna till strukturerade dataframes</li>\n<li>omsamplade tidsseriedata till timintervall</li>\n<li>aggregerade produktionen och beräknade nettoelförbrukningen (produktion + import − export)</li>\n<li>exporterade resultaten som CSV eller läste in dem i en lättviktig SQLite‑databas för inspektion.</li>\n</ul>\n<p>JQ användes för att skapa en snabb lösning med minimala beroenden för kommandoradsmiljöer, byggd som en kedja av JQ‑filter.</p>\n<p>JSONL‑data importerades till PostgreSQL, där normaliserade tabeller skapades och en serie SQL‑frågor skrevs.</p>\n<p>För att utvärdera prestandan benchmarkades varje lösning med historiska dataset över både 1 dag och 30 dagar.</p>\n<p><strong>Resultat.</strong> Python‑implementationen visade sig vara den snabbaste och mest skalbara lösningen och klarade:</p>\n<ul>\n<li>bearbetning av 1 dags data på bara 34 ms</li>\n<li>ett dataset på 30 dagar på 180 ms</li>\n</ul>\n<p>Det bekräftade att den lämpar sig för längre tidsperioder med bibehållen prestanda under en sekund. Den gav också tydlig och underhållbar kod som enkelt kunde byggas ut till bearbetning av flera zoner eller integreras i en ETL‑pipeline.</p>\n<p>Sammantaget visade angreppssättet med flera verktyg på flexibilitet i verktygsvalet, stark prestandaoptimering och robust hantering av tidsjustering och aggregering i verkliga datapipelines för eldata.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Dataanalys", "Databaser", "Datapipelines (ETL/ELT)", "PostgreSQL", "Prestandaoptimering", "Python", "SQL", "Dataanalys och BI‑dashboards", "Optimering av databasprestanda", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/accelerated-geographical-data-pipeline-performance-by-50x-by-5/", "url": "https://engineer.company/sv/portfolio/accelerated-geographical-data-pipeline-performance-by-50x-by-5/", "title": "Gjorde pipelinen för geodata 50 gånger snabbare genom bättre SQL‑programmering och datamodellering i PostgreSQL, MS SQL och Google Cloud BigQuery.", "summary": "Gjorde en geografisk datapipeline 50 gånger snabbare genom att finjustera SQL och datamodell i PostgreSQL, MS SQL och Google BigQuery.", "content_html": "<p><strong>Situation.</strong> Organisationen hade betydande förseningar i bearbetningen av stora mängder geografiska data, vilket påverkade effektiviteten i datadrivna beslutsprocesser och analytisk rapportering.</p>\n<p><strong>Uppgift.</strong> Målet var att optimera pipelinen för geografiska data för att förbättra prestandan och korta bearbetningstiden, så att dataanalyser kunde utföras effektivare och i realtid.</p>\n<p><strong>Handling.</strong> De befintliga strukturerna för SQL‑programmering och datamodellering i PostgreSQL, MS SQL och Google Cloud BigQuery analyserades noggrant, vilket blottlade flaskhalsar i form av ineffektiva indexeringsstrategier, suboptimalt utformade SQL‑frågor och avsaknad av lämpliga integritetsvillkor. För att komma till rätta med problemen:</p>\n<ul>\n<li>Datamodellerna gjordes om för att normalisera kritiska dataset, och partitioneringsstrategier infördes för att korta tiderna för datahämtning.</li>\n<li>Tillämpade avancerade indexeringstekniker, bl.a. B‑tree- och GiST‑index för PostgreSQL, filtrerade index i MS SQL och klustring i BigQuery.</li>\n<li>Optimerade komplexa SQL‑frågor genom att refaktorera underfrågor, minska antalet join‑operationer och använda materialiserade vyer där det var relevant.</li>\n<li>Etablerade integritetsvillkor som främmande nycklar och CHECK‑villkor för att säkerställa datakonsistens utan att ge avkall på prestandan.</li>\n</ul>\n<p><strong>Resultat.</strong> De omfattande optimeringarna gjorde pipelinen för geografiska data 50 gånger snabbare och minskade bearbetningstiden avsevärt. Förbättringen möjliggjorde realtidsanalys, gav bättre rapporteringsmöjligheter och försåg intressenterna med snabba, datadrivna insikter, vilket i slutänden bidrog till bättre underbyggda strategiska beslut.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Databaser", "Datalager", "Datapipelines (ETL/ELT)", "Datastyrning", "GIS / geodata", "PostgreSQL", "Prestandaoptimering", "SQL", "Design av datalager", "GIS- och geodatalösningar", "Optimering av databasprestanda", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/improved-geographical-map-application-performance-by-10x-through-6/", "url": "https://engineer.company/sv/portfolio/improved-geographical-map-application-performance-by-10x-through-6/", "title": "Förbättrade prestandan i en kartapplikation för geodata 10 gånger genom en strategisk databasövergång från MSSQL till PostgreSQL, inklusive själva omställningen av data, och optimerade bearbetningen och datasäkerheten.", "summary": "Gjorde en GIS-kartapplikation 10 gånger snabbare genom migrering från MSSQL till PostgreSQL — spatiala frågor sjönk från 2,5 s till under 250 ms.", "content_html": "<p><strong>Situation.</strong> Organisationens geografiska kartapplikation, som hanterade spatiala databasfrågor i realtid för många användare, led av allvarliga prestandaflaskhalsar. Latensen vid rendering av kartlager och vid frågor mot platsbaserade data påverkade både användarupplevelsen och tillförlitligheten i backendtjänsterna. Systemet byggde på en äldre Microsoft SQL Server‑databas (MSSQL) som saknade inbyggd geospatial indexering.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att förbättra kartapplikationens prestanda, skalbarhet och säkerhet, med det uttalade målet att minska latensen för databasfrågor och öka genomströmningen för spatiala operationer.</p>\n<p><strong>Handling.</strong> Arbetet bestod i att leda en strategisk flytt av databasen från MSSQL till PostgreSQL med tillägget PostGIS, för att få inbyggt stöd för geospatiala data.</p>\n<ul>\n<li>Utformade ett nytt schema optimerat för spatiala data och införde GiST- och SP‑GiST‑index på geometri- och geografikolumner för snabbare frågor.</li>\n<li>Definierade strikta villkor för främmande nycklar och CHECK‑villkor för att säkerställa relationell integritet och upprätthålla valideringsregler för spatiala koordinater.</li>\n<li>Migrerade över 50 miljoner spatiala poster via ETL‑pipelines med transformeringssteg för att anpassa data till nya SRID‑standarder (EPSG:4326).</li>\n<li>Finjusterade konfigurationsparametrarna i PostgreSQL (t.ex. <code>work_mem</code>, <code>effective_cache_size</code>) för optimal I/O‑prestanda vid samtidig åtkomst.</li>\n<li>Implementerade rollbaserad åtkomstkontroll (RBAC) och säkerhet på radnivå för att upprätthålla dataskyddspolicyer för flera användargrupper.</li>\n</ul>\n<p><strong>Resultat.</strong> Prestandan för spatiala databasfrågor förbättrades 10 gånger, och den genomsnittliga svarstiden sjönk från 2,5 sekunder till under 250 millisekunder. CPU‑belastningen i backend minskade med 65 %, och systemets tillgänglighet förbättrades vid toppbelastning. Säkerheten stärktes också genom finkorniga åtkomstpolicyer och valideringsvillkor för data, vilket minskade risken för korrupta spatiala data.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "GIS / geodata", "Migrering och modernisering", "PostgreSQL", "Prestandaoptimering", "SQL", "Säkerhet", "Databasmigrering och modernisering", "GIS- och geodatalösningar", "Optimering av databasprestanda" ] }, { "id": "https://engineer.company/sv/portfolio/resolved-1-000-issues-in-geographical-data-and-7/", "url": "https://engineer.company/sv/portfolio/resolved-1-000-issues-in-geographical-data-and-7/", "title": "Löste 1 000 problem i geodata och tidsseriedata med GDAL, ArcGIS, PostGIS, Mapbox, QGIS, SQL (PL/pgSQL, Transact‑SQL) och Bash, vilket säkrade en högkvalitativ bearbetning av stora datamängder.", "summary": "Löste 1 000+ problem i geografisk big data och tidsserier med GDAL, PostGIS, ArcGIS, Mapbox, QGIS och SQL — 35 % snabbare frågor.", "content_html": "<p><strong>Situation.</strong> Under arbetet med ett storskaligt geospatialt analysprojekt stötte teamet på många inkonsekvenser och avvikelser i de geografiska dataseten och tidsseriedataseten. Problemen påverkade precisionen i de spatiala analyserna och i de beslutsstöd som användes på flera avdelningar.</p>\n<p><strong>Uppgift.</strong> Ansvaret gällde att identifiera, åtgärda och optimera över 1 000 datakvalitetsproblem i dessa komplexa dataset, för att säkerställa integriteten och prestandan i de applikationer och visualiseringar som byggde på dem.</p>\n<p><strong>Handling.</strong> Spatiala fel diagnostiserades och rättades systematiskt med en kombination av verktyg, bl.a. GDAL, QGIS och ArcGIS, och automatiserade arbetsflöden implementerades i Bash‑skript för att effektivisera återkommande datarensning. PostGIS hanterade avancerade spatiala frågor och spatial indexering, och robusta procedurer skrevs i PL/pgSQL och Transact‑SQL för att hantera och transformera både geografiska och tidsberoende data i PostgreSQL- och SQL Server‑databaserna. Dessutom integrerades de rensade dataseten i interaktiva visualiseringar med Mapbox, vilket gjorde data mer tillgängliga för slutanvändarna.</p>\n<p><strong>Resultat.</strong> Genom det arbetet åtgärdades över 1 000 kritiska problem, vilket avsevärt förbättrade noggrannheten i data och bearbetningshastigheten. Det bidrog direkt till 35 % kortare körtider för spatiala frågor och gjorde teamets spatiala analyser mer tillförlitliga. Arbetet säkerställde att högkvalitativa, användningsklara data alltid fanns tillgängliga för analys och rapportering, till stöd för strategiska beslut i hela organisationen.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "Datastyrning", "GIS / geodata", "PostgreSQL", "Prestandaoptimering", "SQL", "Datastyrning och datakvalitet", "GIS- och geodatalösningar", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/", "url": "https://engineer.company/sv/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/", "title": "Utformade, implementerade och administrerade 6 ETL/ELT‑pipelines med Google BigQuery, MSSQL, PostgreSQL, Shell‑skript, PL/pgSQL och Transact‑SQL och integrerade data för effektiv bearbetning i ett Python‑API.", "summary": "Utformade och drev 6 ETL/ELT-pipelines (BigQuery, MSSQL, PostgreSQL) och kortade ett manuellt jobb på 3 timmar till under 20 minuter med daglig synk.", "content_html": "<p><strong>Situation.</strong> Organisationen behövde konsolidera och bearbeta stora volymer strukturerade data från ett fjärrbeläget datalager bakom ett IPsec‑VPN. Dessa data var avgörande för att driva interna analysdashboards och externa Python‑API:er som användes av kunder och partner.</p>\n<p><strong>Uppgift.</strong> Målet var att utforma, implementera och underhålla en uppsättning robusta och automatiserade ETL/ELT‑pipelines för att på ett säkert sätt hämta, transformera och läsa in data i Google BigQuery, och samtidigt säkerställa korrekta data, prestanda och skalbarhet i alla system.</p>\n<p><strong>Handling.</strong> 6 kompletta ETL/ELT‑pipelines utformades, implementerades och administrerades, med en rad tekniker som Google BigQuery, MSSQL, PostgreSQL, Shell‑skript, PL/pgSQL och Transact‑SQL.</p>\n<ul>\n<li>Etablerade säkra anslutningar till en fjärrserver skyddad av ett IPsec‑VPN för att automatisera datahämtningen.</li>\n<li>Schemalade och körde nedladdningar av komprimerade dataarkiv med Parquet-, CSV- och .bak‑filer.</li>\n<li>Utvecklade Bash- och Python‑skript för att extrahera och klassificera filer efter typ och schema.</li>\n<li>För .bak‑filerna återställdes säkerhetskopiorna till en lokal MSSQL‑instans med arbetsflöden baserade på RESTORE DATABASE, och schemaintegriteten validerades.</li>\n<li>Läste in strukturerade data i staging‑scheman i MSSQL och PostgreSQL med verktygen bcp, psql och SSIS, beroende på källformatet.</li>\n<li>Skrev modulära och återanvändbara PL/pgSQL- och T‑SQL‑procedurer för att rensa, normalisera och berika data utifrån affärslogiken.</li>\n<li>Exporterade kurerade dataset och tabeller till mellanformat, komprimerade dem med gzip och överförde dem säkert till en bucket i Google Cloud Storage.</li>\n<li>Automatiserade uppladdningen och schemamappningen till Google BigQuery med bq CLI och Python‑baserade skript för datainläsning.</li>\n</ul>\n<p><strong>Resultat.</strong> Dessa automatiserade pipelines minskade avsevärt det manuella merarbetet och bearbetningstiden — från över 3 timmars manuellt arbete till under 20 minuter för hela flödet, samtidigt som datafärskheten förbättrades från veckovis till daglig synkronisering. De Python‑API:er som använde dessa data fick 30 % bättre prestanda, och den ökade insynen hjälpte affärsanalytikerna att snabbare leverera insikter till intressenterna. Lösningen är fortfarande skalbar och utbyggbar, så att nya datakällor kan anslutas i takt med att verksamheten växer.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Databaser", "Datalager", "Datapipelines (ETL/ELT)", "Molntjänster", "Nätverk och VPN", "PostgreSQL", "Python", "SQL", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Design av datalager", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/developed-and-launched-the-company-s-first-observability-9/", "url": "https://engineer.company/sv/portfolio/developed-and-launched-the-company-s-first-observability-9/", "title": "Utvecklade och lanserade företagets första dashboard för observerbarhet, med realtidsinsikt i systemens prestanda och datavisualisering på den stora tv:n på kontoret.", "summary": "Byggde företagets första dashboard för observerbarhet med realtidsinsyn i systemen, vilket hjälpte teamen att upptäcka och lösa incidenter 40 % snabbare.", "content_html": "<p><strong>Situation.</strong> Företaget hade svårt att övervaka systemens prestanda i realtid, vilket ofta ledde till fördröjd incidenthantering och sämre insyn i infrastrukturens hälsa. Det fanns ingen central lösning där teamen kunde få insikt i nyckeltalen för driften.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att ta fram en lösning som lät både tekniska och icke‑tekniska intressenter övervaka viktiga systemmått i realtid, med fokus på lättillgänglighet, tydlighet och proaktiv upptäckt av problem.</p>\n<p><strong>Handling.</strong> Företagets första dashboard för observerbarhet utformades och implementerades och samlade in alla viktiga systemmått — som CPU, RAM, HDD, temperatur med mera — från Linux‑servrar på distans via SSH. Även Docker‑containrar övervakades på det sättet. Senare utformades och implementerades en andra version med Grafana och Prometheus för mer avancerad visualisering och övervakning. I samarbete med DevOps- och utvecklingsteamen identifierades de kritiska måtten, som CPU‑användning, minnesanvändning, tjänsternas upptid och API‑latens. Datapipelines konfigurerades för att läsa in och bearbeta prestandamått från olika system, och dashboarden driftsattes på en stor tv‑skärm på kontoret för största möjliga synlighet. Larmmekanismer för överskridna tröskelvärden integrerades också, så att åtgärder kunde vidtas omedelbart.</p>\n<p><strong>Resultat.</strong> Dashboarden gav avsevärt bättre insyn i systemen och kortare reaktionstid vid driftproblem. Teamen kunde upptäcka och lösa incidenter 40 % snabbare. Den främjade också en kultur av delat ansvar för systemens hälsa genom att göra prestandadata tillgängliga för alla på kontoret, vilket i slutänden bidrog till en stabilare och effektivare produktionsmiljö.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "Dataanalys", "DevOps", "Infrastruktur", "Linux och servrar", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Dataanalys och BI‑dashboards", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/", "url": "https://engineer.company/sv/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/", "title": "Levererade 8 Power BI‑projekt med utförliga handböcker och integrerade Microsoft Power BI‑verktygen med ett NodeJS‑API och Python FastAPI för effektiv dataanalys och visualisering.", "summary": "Levererade 8 analysprojekt i Power BI (Node.js-API, Python FastAPI) med fullständiga manualer och gjorde rapportgenereringen 60 % effektivare.", "content_html": "<p><strong>Situation.</strong> Organisationen behövde dynamiska, visuella insikter i komplexa dataset om elnätets prestanda och mått på geografisk fördelning, som stöd för beslutsfattandet i både tekniska och strategiska team.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att utveckla interaktiva dashboards och rapportlösningar som på ett effektivt sätt kunde presentera både realtidsdata och historiska geografiska data och eldata, så att intressenterna snabbt kunde se trender, avvikelser och prestandaindikatorer.</p>\n<p><strong>Handling.</strong> Microsoft Power BI integrerades med en egenutvecklad backendstack med NodeJS API och Python FastAPI för att effektivisera inläsning, transformering och visualisering av data. Dashboards med kartvisualiseringar, mått på energiförbrukning, uppföljning av avbrott och indikatorer för nätets effektivitet utformades och implementerades. Återanvändbara mallar och detaljerad dokumentation togs fram som stöd för skalbarhet och användarvänlighet.</p>\n<p><strong>Resultat.</strong> Arbetet förde 8 dataanalysprojekt i mål, effektiviserade rapportgenereringen med 60 % och gjorde det möjligt för tvärfunktionella team att fatta snabbare, datadrivna beslut. Intressenterna rapporterade en betydligt bättre förståelse av elnätets prestanda regionalt och en träffsäkrare resursplanering.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Data engineering", "Dataanalys", "Dokumentation", "GIS / geodata", "Produkt och krav", "Python", "Backend- och API‑utveckling", "Dataanalys och BI‑dashboards", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/released-500-electricity-and-gis-data-analysis-reports-11/", "url": "https://engineer.company/sv/portfolio/released-500-electricity-and-gis-data-analysis-reports-11/", "title": "Publicerade 500 analysrapporter om el- och GIS‑data och använde grundlig research och felsökning för att säkerställa korrekta insikter ur stora mängder geodata och tidsseriedata.", "summary": "Publicerade 500 analysrapporter om el- och GIS-data — en gemensam referens för avdelningarna inom lastbalansering, effektivitet och avvikelsedetektering.", "content_html": "<p><strong>Situation.</strong> Teamet förvaltade enorma dataset som genererades av smarta elmätare installerade i flera geografiska regioner. De smarta mätarna producerade detaljerade tidsseriedata om elförbrukning, som energianalytiker, ingenjörer och regionala planerare använde för operativa och strategiska beslut.</p>\n<p><strong>Uppgift.</strong> Ansvaret gällde att ta fram högkvalitativa, transparenta och reproducerbara analysrapporter som kunde blottlägga mönster i energiförbrukningen, upptäcka avvikelser och identifiera regionala förbrukningstrender, och samtidigt se till att icke‑tekniska intressenter lätt kunde tolka och återanvända resultaten.</p>\n<p><strong>Handling.</strong> Över 500 fördjupade dataanalysrapporter togs fram och levererades, där all extrahering, transformering och analys av data gjordes i ren SQL, direkt i molnbaserade miljöer som PostgreSQL och BigQuery. Dataunderlaget omfattade geografiska koordinater, mätar‑ID:n, tidsstämplad energiförbrukning och miljömetadata. SQL‑skripten använde CTE:er, fönsterfunktioner, underfrågor och geospatiala joins, vilket gav skalbar och effektiv bearbetning.</p>\n<p>Varje rapport innehöll kommenterad SQL‑kod, så att kollegor och samarbetspartner fullt ut kunde reproducera och granska undersökningen, vilket avsevärt minskade tiden för uppföljande analyser. Felsökningsanteckningar lades också till, och vanliga datakvalitetsproblem dokumenterades, t.ex. saknade GPS‑koordinater eller korrupta mätvärden, tillsammans med rekommenderade rutiner för hur de skulle hanteras.</p>\n<p><strong>Resultat.</strong> Rapporterna blev ett standardunderlag på flera avdelningar och bidrog till regional lastbalansering, planering av energieffektivitet och upptäckt av avvikelser. Genom full transparens och reproducerbarhet stärkte arbetet intressenternas förtroende för data och bidrog till träffsäkrare prognosmodeller och 10–15 % effektivare operativ planering.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dataanalys", "Databaser", "Datalager", "Datastyrning", "Dokumentation", "GIS / geodata", "Intressenter och rapportering", "PostgreSQL", "SQL", "Dataanalys och BI‑dashboards", "Datastyrning och datakvalitet", "GIS- och geodatalösningar" ] }, { "id": "https://engineer.company/sv/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/", "url": "https://engineer.company/sv/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/", "title": "Utformade, byggde upp och förvaltade 100 datalagerdatabaser i PostgreSQL, MS SQL och Google BigQuery, främst med GIS- och tidsseriedata, och optimerade prestanda och skalbarhet.", "summary": "Utformade och drev 100 datalagerdatabaser i PostgreSQL, MS SQL och BigQuery för GIS- och tidsseriedata — frågetiderna sjönk 40–60 %.", "content_html": "<p><strong>Situation.</strong> I ett snabbväxande techföretag fanns ett kritiskt behov av att skapa, förvalta och optimera en varierad portfölj med över 100 databaser i PostgreSQL, Microsoft SQL Server och Google BigQuery. Databaserna hanterade främst komplexa dataset, bl.a. data från geografiska informationssystem, GIS (t.ex. spatiala koordinater och platsbaserad analys), och tidsseriedata (t.ex. sensoravläsningar, loggar och mätvärden i realtid). Den befintliga infrastrukturen hade problem med skalbarhet, prestanda för databasfrågor och datakonsistens, särskilt när datavolymen växte exponentiellt. Organisationen behövde en robust arkitektur för att säkerställa tillförlitliga data och minska driftkostnaderna.</p>\n<p><strong>Uppgift.</strong> Huvudmålet var att utforma, implementera och förvalta ett skalbart, högpresterande ekosystem för datalager, anpassat för GIS- och tidsseriedata. Det innebar:</p>\n<ul>\n<li>Åtgärder mot prestandaflaskhalsar i komplexa spatiala och tidsbaserade databasfrågor.</li>\n<li>Skalbarhet för växande datavolymer med bibehållen kostnadseffektivitet.</li>\n<li>Samarbete med tvärfunktionella team (t.ex. data scientists, produktchefer) för att anpassa databasdesignen efter verksamhetens behov.</li>\n</ul>\n<p><strong>Handling.</strong> För att nå dessa mål:</p>\n<ol>\n<li>Utformade skalbara arkitekturer:</li>\n</ol>\n<ul>\n<li>Skapade normaliserade och denormaliserade scheman för PostgreSQL och SQL Server, med spatial indexering (t.ex. PostGIS för PostgreSQL) och tidsseriepartitionering för att optimera prestandan för databasfrågor.</li>\n<li>Använde tidspartitionerade och klustrade tabeller i BigQuery för effektiv hantering av storskaliga tidsseriedata.</li>\n</ul>\n<ol start=\"2\">\n<li>Implementerade optimeringsstrategier:</li>\n</ol>\n<ul>\n<li>Införde tekniker för frågeoptimering, som indexering, materialiserade vyer och cachning, för att minska latensen för GIS- och tidsseriefrågor.</li>\n<li>Tillämpade datakomprimering och kolumnlagring i BigQuery för att minimera lagringskostnaderna och snabba upp skanningarna.</li>\n</ul>\n<ol start=\"3\">\n<li>Samarbetade kring plattformsöverskridande integration:</li>\n</ol>\n<ul>\n<li>Dokumenterade bästa praxis för datamodellering av GIS- och tidsseriedata som vägledning för teamen i framtida projekt.</li>\n</ul>\n<p><strong>Resultat.</strong> Initiativen ledde till betydande förbättringar:</p>\n<ul>\n<li>Prestandavinster: Svarstiderna för databasfrågor mot GIS- och tidsseriedata minskade med 40–60 %, vilket gav snabbare analyser och snabbare beslut.</li>\n<li>Operativ tillförlitlighet: Automatiserad övervakning minskade driftstoppen med 50 %, samtidigt som standardiserade processer höjde teamets produktivitet och minskade antalet fel.</li>\n<li>Affärsnytta: Den optimerade infrastrukturen gjorde det möjligt för företaget att lansera nya datadrivna produkter (t.ex. dashboards för realtidsanalys) och uppfylla regulatoriska krav på datastyrning.</li>\n</ul>\n<p>Arbetet befäste organisationens förmåga att hantera komplexa datautmaningar.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Databaser", "Datalager", "Datastyrning", "GIS / geodata", "Plattformsarkitektur", "PostgreSQL", "Prestandaoptimering", "SQL", "Tillförlitlighet och säkerhetskopiering", "Databasadministration (DBA)", "Databasdesign och datamodellering", "Design av datalager", "GIS- och geodatalösningar", "Optimering av databasprestanda" ] }, { "id": "https://engineer.company/sv/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/", "url": "https://engineer.company/sv/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/", "title": "Utformade, driftsatte och underhöll 10 PostgreSQL- och MS SQL‑servrar på VPS:er med Ubuntu Linux och säkerställde optimal serverprestanda och tillförlitlighet.", "summary": "Utformade och underhöll 10 PostgreSQL- och MS SQL-servrar på Ubuntu Linux VPS med 99,9 % upptid och 30 % snabbare frågor genom finjustering.", "content_html": "<p><strong>Situation.</strong> I rollen som DevOps‑ingenjör på ett medelstort techföretag gällde uppgiften att förvalta och optimera databasinfrastrukturen för att stödja en växande användarbas och affärskritiska applikationer. Organisationen var starkt beroende av PostgreSQL och Microsoft SQL Server för datalagring och analys, med krav på hög tillgänglighet, skalbarhet och säkerhet.</p>\n<p><strong>Uppgift.</strong> Huvudansvaret var att utforma arkitekturen för, driftsätta, konfigurera och underhålla 10 PostgreSQL- och MS SQL Server‑instanser i VPS‑miljöer med Ubuntu Linux. Det omfattade att säkerställa optimal prestanda, införa robusta säkerhetsprotokoll och etablera proaktiv övervakning för att förebygga avbrott. Dessutom ingick att skala infrastrukturen för framtida tillväxt och samtidigt hålla nere kostnaderna och följa branschstandarderna.</p>\n<p><strong>Handling.</strong> 1. Driftsättning och konfiguration:</p>\n<ul>\n<li>Installerade och konfigurerade PostgreSQL 14 och MS SQL Server 2019 på VPS‑instanser med Ubuntu 20.04 LTS och säkerställde kompatibiliteten med företagets applikationer.</li>\n<li>Satte upp automatiserade säkerhetskopior med <code>pg_dump</code> för PostgreSQL och SQL Server Agent‑jobb för MS SQL, med regler för lagringstid och kopior lagrade på annan plats.</li>\n<li>Optimerade serverkonfigurationerna (t.ex. minnesallokering, cachning av databasfrågor och anslutningspoolning) för att förbättra prestandan för databasfrågor och minska latensen.</li>\n</ul>\n<ol start=\"2\">\n<li>\n<p>Övervakning och underhåll:</p>\n<ul>\n<li>Implementerade övervakningsverktyg som Prometheus och Grafana för att följa CPU, minne, disk‑I/O och prestandamått för databasfrågor i realtid.</li>\n<li>Genomförde regelbunden patchning och uppdatering av både databaserna och operativsystemet Ubuntu för att åtgärda sårbarheter och säkerställa regelefterlevnad.</li>\n<li>Skapade egna skript för logganalys.</li>\n</ul>\n</li>\n<li>\n<p>Säkerhet och skalbarhet:</p>\n<ul>\n<li>Konfigurerade brandväggar (UFW) och upprätthöll rollbaserad åtkomstkontroll (RBAC) för att skydda känsliga data.</li>\n<li>Dokumenterade rutiner för katastrofåterställning, bl.a. återställning till en given tidpunkt och instruktioner för failover.</li>\n</ul>\n</li>\n</ol>\n<p><strong>Resultat.</strong> - Uppnådde 99,9 % upptid på alla 10 databasservrar.</p>\n<ul>\n<li>Förbättrade svarstiderna för databasfrågor med 30 % genom konfigurationsjustering och indexoptimering, vilket gav bättre prestanda i applikationerna.</li>\n<li>Minskade de manuella underhållsuppgifterna med 50 % genom automatisering, vilket frigjorde över 10 timmar per månad för strategiska projekt.</li>\n<li>Skalade infrastrukturen så att den klarade 40 % mer användartrafik utan försämrad tjänstekvalitet, vilket bidrog till 20 % högre intäkter under det följande kvartalet.</li>\n<li>Fick erkännande av CTO:n för att ha infört bästa praxis för säkerhet som förhindrade potentiella dataintrång.</li>\n</ul>\n<p>Erfarenheten befäste en djup expertis inom databashantering, DevOps‑automatisering och infrastrukturoptimering och gav tillförlitliga, säkra och skalbara lösningar för komplexa företagsmiljöer.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Databaser", "DevOps", "Infrastruktur", "Linux och servrar", "PostgreSQL", "Prestandaoptimering", "Systemadministration", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Databasadministration (DBA)", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/", "url": "https://engineer.company/sv/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/", "title": "Stärkte datasäkerheten genom att införa 1 000 RBAC‑regler för utvecklare, applikationsinstanser, PostgreSQL, MS SQL och andra Linux‑servrar, vilket förhindrade obehörig åtkomst; dokumenterade reglerna som automatisering i Ansible.", "summary": "Härdade säkerheten med 1 000 RBAC-regler för utvecklare, appar och Linux-/DB-servrar, automatiserat med Ansible — risken för obehörig åtkomst sjönk 85 %.", "content_html": "<p><strong>Situation.</strong> Organisationen behövde stärka åtkomstkontrollen i flera system, bl.a. utvecklarmiljöer, applikationsinstanser, PostgreSQL, MS SQL och Linux‑servrar. Själva RBAC‑modellen var okomplicerad, men utmaningen låg i att hantera över 1 000 enskilda regler för att täcka in olika användarroller och systemkrav. Målet var att säkerställa strikta åtkomstbegränsningar utan att tillföra komplexitet.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att implementera en skalbar RBAC‑lösning genom att definiera och upprätthålla över 1 000 regler för åtkomstkontroll. Det innebar att knyta behörigheter till specifika roller (t.ex. utvecklare, applikationsinstanser, databasadministratörer) och se till att reglerna tillämpades konsekvent i alla system. Uppgiften krävde också att reglerna dokumenterades och att driftsättningen av dem automatiserades för att undvika manuella fel.</p>\n<p><strong>Handling.</strong> Angreppssättet byggde på en enkel, modulär RBAC‑struktur där behörigheterna delades upp i tydliga, återanvändbara kategorier (t.ex. ”skrivskyddad åtkomst till produktionsdatabaser”). Med Ansible automatiserades konfigurationen av varje regel, vilket gav konsekvens mellan miljöerna. Utvecklare fick till exempel bara åtkomst till sina tilldelade servrar, medan applikationsinstanserna hade begränsade behörigheter för att förhindra lateral förflyttning. Processen prioriterade tydlighet framför komplexitet, och varje regel var uttryckligen knuten till en specifik roll och ett specifikt system.</p>\n<p><strong>Resultat.</strong> Implementeringen säkrade över 1 000 regler utan onödig komplexitet och minskade risken för obehörig åtkomst med 85 %. Automatiseringen effektiviserade driftsättningen och kortade uppsättningstiden med 60 % jämfört med manuella metoder. Det dokumenterade ramverket gjorde att teamen snabbt kunde granska eller ändra regler, vilket säkrade skalbarheten i takt med att infrastrukturen växte. Genom fokus på enkelhet och volym gav lösningen robust säkerhet med bibehållen operativ effektivitet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Databaser", "DevOps", "Dokumentation", "Infrastruktur", "Linux och servrar", "Systemadministration", "Säkerhet", "Databasadministration (DBA)", "Infrastruktur som kod", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/", "url": "https://engineer.company/sv/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/", "title": "Gjorde PostgreSQL 10 gånger snabbare med strategisk indexering, partitionering och optimering av databasfrågor och effektiviserade databasen för användar- och tenantdata, geospatiala data och tidsseriedata om el.", "summary": "Gjorde PostgreSQL 10 gånger snabbare med indexering, partitionering och frågeoptimering för användar-, tenant-, geo- och tidsseriedata — 40 % lägre resursbruk.", "content_html": "<p><strong>Situation.</strong> PostgreSQL‑databasen betjänade en medelstor applikation som hanterade användarkonton, information om tenants, geografiska data (t.ex. platser, regioner) och dagliga mätvärden för elförbrukning. Systemet hade låg trafik, men prestandaproblem uppstod i takt med att datavolymerna växte. Databasfrågor mot geospatiala data och tidsserieloggar för el blev tröga, vilket gav ojämna svarstider. Avsaknaden av optimerad indexering, fragmenterade databasfrågor och opartitionerade tabeller förvärrade problemet och skapade flaskhalsar i kritiska funktioner som användarautentisering, hantering av tenants och datarapportering.</p>\n<p><strong>Uppgift.</strong> Målet var att förbättra databasens prestanda 10 gånger utan att göra om den befintliga arkitekturen. Fokus låg på att optimera körningen av databasfrågor, minska latensen och säkerställa skalbarhet för framtida datatillväxt. Viktiga prioriteringar var kortare svarstider för geospatiala och tidsbaserade databasfrågor, minimal resurskonkurrens och bibehållen dataintegritet medan ändringarna genomfördes.</p>\n<p><strong>Handling.</strong> 1. <strong>Indexering:</strong> Analyserade kolumner som ofta användes i databasfrågor (t.ex. användar‑ID, tenant‑ID, geospatiala koordinater) och skapade riktade index. För geospatiala data lades GiST‑index till för att snabba upp spatiala frågor. Sammansatta index infördes för filter på flera kolumner, t.ex. tenant‑ID + datumintervall för eldata.\n2. <strong>Partitionering:</strong> Implementerade tidsbaserad intervallpartitionering för tabellen med eldata, uppdelad per dag/månad. Det minskade datamängden för varje databasfråga och förbättrade skanningseffektiviteten. För geografiska data användes hashpartitionering för att fördela lasten jämnt mellan noderna.\n3. <strong>Frågeoptimering:</strong> Skrev om komplexa databasfrågor för att undvika fullständiga tabellskanningar, med CTE:er (Common Table Expressions) och materialiserade vyer för ofta använda dataset. Verktyget <code>EXPLAIN ANALYZE</code> pekade ut ineffektiva joins och underfrågor, som strukturerades om för bättre exekveringsplaner. Dessutom konfigurerades cachning av databasfrågor och anslutningspoolning för att minska overhead.</p>\n<p><strong>Resultat.</strong> Efter optimeringen förbättrades svarstiderna för databasfrågor 10 gånger, och kritiska operationer (t.ex. användarautentisering, geospatiala uppslag) kördes på millisekunder. Databasens resursanvändning minskade med 40 %, så att systemet klarade större datavolymer utan försämrad prestanda. Användarna rapporterade smidigare interaktion, och systemet blev mer skalbart för framtida tillväxt. Ändringarna minskade också behovet av hårdvaruuppgraderingar, vilket sparade kostnader och säkrade tillförlitligheten på lång sikt. Projektet visade hur strategisk indexering, partitionering och förfining av databasfrågor kan förvandla även system med låg last till effektiva, framtidssäkra databaser.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Databaser", "GIS / geodata", "PostgreSQL", "Prestandaoptimering", "SQL", "Databasadministration (DBA)", "Optimering av databasprestanda" ] }, { "id": "https://engineer.company/sv/portfolio/automated-gis-saas-application-deployment-data-processing-and-16/", "url": "https://engineer.company/sv/portfolio/automated-gis-saas-application-deployment-data-processing-and-16/", "title": "Automatiserade driftsättningen av en GIS‑applikation som SaaS, databearbetningen och rapporteringssystemet med GitHub Actions CI/CD, Python, Bash och SQL.", "summary": "Automatiserade driftsättning, databehandling och rapportering för en GIS-SaaS med GitHub Actions CI/CD, Python, Bash och SQL — kortare, tillförlitliga releaser.", "content_html": "<p><strong>Situation.</strong> Driftsättningen av SaaS‑appen för GIS, bearbetningen av dess data och framtagningen av rapporterna var alla manuella steg — och manuella steg är både långsamma och försåtligt farliga. Varje release åt utvecklingstid och bar på risken för ett misstag, och det återkommande data- och rapportarbetet låg där och åt kapacitet vecka efter vecka.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att automatisera hela vägen från kod till produktion, liksom den återkommande databearbetningen och rapporteringen — med målet att releaser skulle vara snabba, säkra och repeterbara i stället för en noggrann manuell ritual varje gång.</p>\n<p><strong>Handling.</strong> Hela vägen automatiserades. Pipelines i GitHub Actions tog över hela cykeln med test, bygge och driftsättning, så att en release inte längre hängde på att någon kom ihåg stegen. Den återkommande databearbetningen och rapporterna flyttades till schemalagda jobb i Python, Bash och SQL, så att de helt enkelt kördes i stället för att vara någons syssla. Och konfigurationen och hemligheterna standardiserades så att varje miljö betedde sig identiskt — vilket är det som tar död på överraskningarna av typen ”det funkar på min maskin”, eftersom det inte längre finns någon ”min maskin” som skiljer sig från produktion.</p>\n<p><strong>Resultat.</strong> Driftsättning, databearbetning och rapportering blev alla automatiserade och tillförlitliga, det manuella slitet lyftes från teamets bord och releasecykeln blev kortare. Teamet kunde lägga sin uppmärksamhet på produkten i stället för på driften runt omkring den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Datapipelines (ETL/ELT)", "DevOps", "GIS / geodata", "Molntjänster", "Python", "SQL", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "GIS- och geodatalösningar", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/automated-delivery-of-20-gis-data-pipelines-and-17/", "url": "https://engineer.company/sv/portfolio/automated-delivery-of-20-gis-data-pipelines-and-17/", "title": "Automatiserade leveransen av 20 GIS‑datapipelines och ETL‑processer för applikationsdata, vilket effektiviserade infrastrukturautomatiseringen och rapporteringen.", "summary": "Automatiserade 20 GIS-datapipelines och ETL-processer för appdata, effektiviserade infrastrukturautomatiseringen och levererade tillförlitlig, aktuell data.", "content_html": "<p><strong>Situation.</strong> Plattformen byggde på många GIS‑datapipelines och ETL‑processer för applikationsdata, och de levererades och övervakades för hand. Handkörda pipelines skapar flaskhalsar, de blir med tiden inkonsekventa, och värst av allt bär de på en ständig låg risk att någon av dem fallerar i det tysta utan att någon märker det förrän data redan är fel längre ned i kedjan.</p>\n<p><strong>Uppgift.</strong> Att automatisera leveransen av dessa pipelines och ETL‑processer — så att data flödade tillförlitligt och förutsägbart utan att någon behövde valla dem — var uppgiften.</p>\n<p><strong>Handling.</strong> Tjugo GIS‑datapipelines och ETL‑flödet för applikationsdata fick automatiserad leverans, hela vägen. Schemaläggningen, loggningen och felhanteringen standardiserades, så att varje pipeline betedde sig likadant och, avgörande nog, så att det syntes när en inte gjorde det — ett tyst fel är bara tyst om ingenting håller uppsikt. Och de lades in i den befintliga infrastrukturautomatiseringen och rapporteringen, så att de blev en del av ett sammanhängande system i stället för en låda full av skript som någon måste komma ihåg att köra.</p>\n<p><strong>Resultat.</strong> Alla tjugo pipelines och deras ETL kördes automatiskt och förutsägbart, och hela bilden av infrastrukturautomatisering och rapportering blev prydligare av det. Verksamheten fick pålitliga, aktuella data utan att någon behövde lotsa dem igenom för hand — och utan att risken för tysta fel hängde över den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Datapipelines (ETL/ELT)", "DevOps", "GIS / geodata", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "DevOps och CI/CD‑automatisering", "GIS- och geodatalösningar", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/automated-100-critical-data-backups-using-barman-google-18/", "url": "https://engineer.company/sv/portfolio/automated-100-critical-data-backups-using-barman-google-18/", "title": "Automatiserade 100 kritiska säkerhetskopior av data med Barman, Google Cloud, Bash och Python och säkerställde dataintegriteten i samtliga databaser.", "summary": "Automatiserade säkerhetskopiering av 100 kritiska databaser med Barman, Google Cloud, Bash och Python — att data går att återställa blev ett testat faktum.", "content_html": "<p><strong>Situation.</strong> Kritiska GIS‑data, applikationsinstanserna och databaserna låg utspridda över olika system, med säkerhetskopiering som var inkonsekvent och delvis manuell. För en produkt som lever på sina data är det ingen risk man kan låta ligga — och den dag man faktiskt behöver en säkerhetskopia är precis den sämsta dagen att upptäcka att den var ofullständig.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att garantera att alla kritiska data gick att återställa, vilket innebar att automatisera heltäckande, verifierade säkerhetskopior i hela miljön — och ordet som spelar roll är verifierade.</p>\n<p><strong>Handling.</strong> Rutinerna för säkerhetskopiering byggdes upp från början till slut. Hundra kritiska säkerhetskopior av data automatiserades, med Barman för PostgreSQL‑delen och Google Cloud för kopiorna på annan plats, och helheten orkestrerades och validerades med Bash och Python — för en säkerhetskopia som har tagits men aldrig kontrollerats är egentligen ingen säkerhetskopia, utan en förhoppning. Därför fanns det regler för lagringstid som höll dem aktuella och integritetskontroller som bekräftade att var och en faktiskt var bra, inte bara fanns där.</p>\n<p><strong>Resultat.</strong> Säkerhetskopieringen kördes automatiskt och gick att verifiera för varje databas, vilket gjorde återställbarheten av data till något testat i stället för ett antagande. En stor operativ risk lyftes från verksamheten och ersattes med en återställningsväg som faktiskt gick att lita på — skillnaden var att den här hade kontrollerats, inte bara konfigurerats.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Databaser", "DevOps", "Infrastruktur", "Molntjänster", "PostgreSQL", "Python", "Tillförlitlighet och säkerhetskopiering", "Databasadministration (DBA)", "DevOps och CI/CD‑automatisering", "Säkerhetskopiering och återställning" ] }, { "id": "https://engineer.company/sv/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/", "url": "https://engineer.company/sv/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/", "title": "Driftsatte och underhöll 20 containeriserade Docker‑applikationer, felsökte med Podman och Kubernetes och förvaltade R‑baserade appar på Google Cloud och AWS.", "summary": "Driftsatte och underhöll 20 containerbaserade Docker-appar i Google Cloud och AWS och felsökte med Podman och Kubernetes för stabila releaser.", "content_html": "<p><strong>Situation.</strong> Det fanns en växande samling containeriserade applikationer — däribland några R‑baserade analysappar — som kördes i både Google Cloud och AWS. Utspridda över två moln och några körmiljöer för containrar behövde de driftsättas konsekvent och kunna felsökas snabbt när något gick fel, vilket är svårare än det låter när inga två miljöer är riktigt likadana.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att driftsätta och underhålla dessa arbetslaster tillförlitligt och att snabbt kunna diagnostisera problem i alla körmiljöer och moln.</p>\n<p><strong>Handling.</strong> Containermiljön förvaltades i båda molnen — tjugo Docker‑applikationer driftsattes och underhölls med enhetlig konfiguration och övervakning, så att ingen av dem blev sin egen snöflinga. När något gick fel användes Podman för felsökningen och Kubernetes för att felsöka orkestreringen. De R‑baserade analysapparna fick särskild uppmärksamhet i produktionsmiljöerna i Google Cloud och AWS och hölls stabila och reproducerbara, vilket spelar roll för analys — ett resultat som inte går att reproducera är inte mycket till resultat.</p>\n<p><strong>Resultat.</strong> Containermiljön fungerade tillförlitligt i båda molnen, problem diagnostiserades snabbare och driftsättningarna förblev stabila och reproducerbara. De applikationer som produkten var beroende av förblev pålitliga, oavsett i vilket moln de råkade köras.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "Dataanalys", "DevOps", "Infrastruktur", "Molntjänster", "Tillförlitlighet och säkerhetskopiering", "Containerisering och orkestrering", "DevOps och CI/CD‑automatisering", "Molninfrastruktur och migrering" ] }, { "id": "https://engineer.company/sv/portfolio/managed-30-ubuntu-linux-vps-instances-implementing-disaster-20/", "url": "https://engineer.company/sv/portfolio/managed-30-ubuntu-linux-vps-instances-implementing-disaster-20/", "title": "Förvaltade 30 VPS‑instanser med Ubuntu Linux, införde strategier för katastrofåterställning och säkerställde optimala nätverkskonfigurationer.", "summary": "Förvaltade 30 VPS-instanser med Ubuntu Linux med testade strategier för katastrofåterställning och stabila nätverk — en motståndskraftig grund att växa på.", "content_html": "<p><strong>Situation.</strong> Företagets verksamhet byggde på en park av VPS‑instanser med Ubuntu Linux, vars uppsättning hade vuxit organiskt med tiden — vilket är ett artigt sätt att säga att den hade ackumulerats snarare än utformats. Det lämnade luckor: inkonsekventa konfigurationer, och lösningar för återställning och nätverk som var mer historiska tillfälligheter än plan.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att få ordning på förvaltningen av parken, härda katastrofåterställningen och göra nätverkskonfigurationen konsekvent och förnuftig på varje instans.</p>\n<p><strong>Handling.</strong> De trettio instanserna kom under medveten förvaltning — behandlade som en sammanhållen park snarare än trettio enskilda husdjur. En verklig katastrofåterställning kom på plats: säkerhetskopior, och återställningsrutiner som faktiskt testades, eftersom en otestad återställning bara är en teori. Och nätverkskonfigurationen standardiserades för säkerhet och prestanda, så att varje instans följde samma härdade baskonfiguration i stället för det den råkat hamna med.</p>\n<p><strong>Resultat.</strong> Infrastrukturen blev motståndskraftig och konsekvent, med testade återställningsvägar och ett nätverk som gick att lita på. Risken för driftstopp minskade, och verksamheten fick en pålitlig grund att växa på i stället för ett lapptäcke som hela tiden behövde lappas och lagas.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "DevOps", "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Systemadministration", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Nätverk och VPN‑uppsättning", "Säkerhetskopiering och återställning" ] }, { "id": "https://engineer.company/sv/portfolio/prevented-security-breaches-by-leading-access-management-initiatives-21/", "url": "https://engineer.company/sv/portfolio/prevented-security-breaches-by-leading-access-management-initiatives-21/", "title": "Förhindrade säkerhetsintrång genom att leda initiativ för behörighetshantering med M365, 1Password, Red Hat SSO och OKTA SSO.", "summary": "Ledde åtkomsthanteringen med M365, 1Password, Red Hat SSO och OKTA, minskade risken för obehörig åtkomst kraftigt och gjorde åtkomsten granskningsbar.", "content_html": "<p><strong>Situation.</strong> Åtkomsten till system och tjänster i företaget hanterades inkonsekvent — behörigheter delades ut ad hoc, över tid, av olika personer. Det är ett dubbelt problem: det öppnar för åtkomst som ingen har avsett, och det gör granskning nästan omöjlig, eftersom ingen faktiskt kan säga vem som kommer åt vad, eller varför.</p>\n<p><strong>Uppgift.</strong> Målet var att täppa till säkerhetsexponeringen genom att centralisera och skärpa behörighetshanteringen i hela organisationen.</p>\n<p><strong>Handling.</strong> Översynen samlade identiteterna och åtkomsten i M365, 1Password, Red Hat SSO och OKTA SSO, så att det fanns en sammanhängande bild i stället för utspridda behörigheter per system. Principen om minsta behörighet upprätthölls — människor och system fick exakt det de behövde och inget överflödigt — och onboarding och offboarding standardiserades, så att åtkomst beviljades och, lika viktigt, återkallades snabbt och konsekvent i stället för att ligga kvar efter att någon hade gått vidare.</p>\n<p><strong>Resultat.</strong> Risken för obehörig åtkomst sjönk kraftigt, och åtkomsten blev granskningsbar och konsekvent — äntligen gick det att svara på ”vem kommer åt det här, och varför”. Det märkliga är att det också gjorde vardagen enklare för teamet: rätt dörrar öppnades lätt och fel dörrar förblev stängda, vilket är precis hur bra behörighetshantering faktiskt känns.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Molntjänster", "Systemadministration", "Säkerhet", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/mitigated-operational-risks-by-implementing-a-monitoring-dashboard-22/", "url": "https://engineer.company/sv/portfolio/mitigated-operational-risks-by-implementing-a-monitoring-dashboard-22/", "title": "Minskade driftriskerna genom att införa en dashboard för övervakning med Grafana och Prometheus, vilket förbättrade systemets tillförlitlighet.", "summary": "Byggde en övervakningsdashboard med Grafana + Prometheus som fångar problem innan de eskalerar och flyttade teamet från brandsläckning till förebyggande arbete.", "content_html": "<p><strong>Situation.</strong> Problem upptäcktes oftast först när de redan hade drabbat användarna, eftersom det inte fanns någon samlad bild av hur systemen mådde. Utan den överblicken var teamet ständigt på defensiven — det reagerade på saker som redan hade gått fel i stället för att se dem komma.</p>\n<p><strong>Uppgift.</strong> Målet var att minska den operativa risken genom att ge teamet insyn i realtid i de system det var beroende av.</p>\n<p><strong>Handling.</strong> Observerbarhetslagret byggdes ut. En övervakningsdashboard i Grafana och Prometheus, de viktigaste tjänsterna instrumenterade och — den del som faktiskt spelar roll — mätvärden som betydde något i stället för siffror för syns skull som ser livliga ut och inte säger någonting. Sedan larmtrösklar satta för dem, synliga där teamet faktiskt skulle se dem och kunde agera medan det fortfarande fanns tid att agera.</p>\n<p><strong>Resultat.</strong> Problem började fångas upp och åtgärdas innan de eskalerade, och systemens tillförlitlighet förbättrades av det. Teamet gick från att reaktivt släcka bränder till något lugnare och mer proaktivt — att fånga problemen medan de ännu var små nog att vara tråkiga.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "DevOps och CI/CD‑automatisering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/managed-and-troubleshooted-8-wireguard-vpn-and-ipsec-23/", "url": "https://engineer.company/sv/portfolio/managed-and-troubleshooted-8-wireguard-vpn-and-ipsec-23/", "title": "Förvaltade och felsökte 8 VPN‑anslutningar över WireGuard och IPSEC och garanterade säker kommunikation i Google Cloud och på Linux‑system.", "summary": "Förvaltade och felsökte 8 VPN-anslutningar med WireGuard och IPSEC i Google Cloud och Linux och satte stopp för återkommande avbrott i uppkopplingen.", "content_html": "<p><strong>Situation.</strong> Den säkra förbindelsen mellan molnet och de lokalt driftade Linux‑systemen gick över flera VPN‑tunnlar, och de var bräckliga och besvärliga att diagnostisera när de gick ned. En död tunnel kunde bryta kommunikationen mellan miljöerna, och att felsöka en sådan var långsamt och osäkert — man var aldrig riktigt säker på att man hade hittat den verkliga orsaken.</p>\n<p><strong>Uppgift.</strong> Att förvalta och felsöka de förbindelserna, för att garantera säker och oavbruten kommunikation, var uppgiften.</p>\n<p><strong>Handling.</strong> VPN‑miljön kom under kontroll — åtta tunnlar med WireGuard och IPsec mellan Google Cloud och Linux‑systemen, förvaltade och felsökta som en helhet i stället för som åtta separata mysterier. Konfigurationen standardiserades så att tunnlarna blev konsekventa och begripliga i stället för att var och en var ett specialfall, och deras tillstånd övervakades, med de återkommande problemen med routning och nyckelutbyte åtgärdade vid roten i stället för att slätas över med en omstart.</p>\n<p><strong>Resultat.</strong> Alla åtta tunnlarna gick säkert och tillförlitligt, kommunikationen mellan miljöerna förblev skyddad, och de återkommande anslutningsincidenterna som brukade avbryta arbetet upphörde. Att åtgärda grundorsakerna i stället för att lindra symtomen var det som förvandlade dem från en återkommande huvudvärk till något som bara fungerade.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Molntjänster", "Nätverk och VPN", "Systemadministration", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Nätverk och VPN‑uppsättning", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/led-the-software-development-of-a-gis-map-24/", "url": "https://engineer.company/sv/portfolio/led-the-software-development-of-a-gis-map-24/", "title": "Ledde mjukvaruutvecklingen av en GIS‑kartapplikation, ökade intäkterna 10 gånger och positionerade produkten som en primär datatillgång.", "summary": "Ledde utvecklingen av en GIS-kartapplikation som blev en hörnsten i plattformen och drev fram 10 gånger högre intäkter via merförsäljning och datalicenser.", "content_html": "<p><strong>Situation.</strong> Den centrala utmaningen var att bygga en plattform som kunde spåra, övervaka och optimera anläggningar för förnybar energi, som solpaneler och vindkraftverk. Den ursprungliga lösningen saknade dock robusta geospatiala funktioner, vilket gjorde det svårt för kunderna att visualisera var anläggningarna fanns, analysera spatiala data eller dra insikter att agera på. Ledningsgruppen såg luckan och prioriterade utvecklingen av en kartapplikation för GIS (geografiskt informationssystem) för att höja plattformens värde och möta kundernas föränderliga behov.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att leda utvecklingen av GIS‑applikationen, en kritisk komponent för att särskilja produkten på den konkurrensutsatta marknaden för grön energi. Rollen sträckte sig bortom mjukvaruutveckling — den spände över rollerna som teknisk ledare, DevOps‑ingenjör, dataingenjör och SRE (Site Reliability Engineer), för att säkerställa att lösningen låg i linje med företagets tillväxtkurva. Målet var att skapa ett skalbart, intuitivt GIS‑verktyg som integrerades sömlöst med SaaS‑plattformen och gjorde det möjligt för användarna att visualisera var anläggningarna fanns, följa hur de presterade och använda spatiala data som beslutsunderlag. Det krävde en balans mellan teknisk innovation och de begränsningar som gäller för en växande startup, samtidigt som produkten måste kunna utvecklas i takt med företaget.</p>\n<p><strong>Handling.</strong> Arbetet inleddes med att i samarbete med intressenterna definiera GIS‑applikationens kärnfunktioner, med fokus på integrationen med den befintliga SaaS‑plattformen och datavisualisering i realtid. Med tanke på det lilla teamet utformades en modulär arkitektur med GIS‑bibliotek med öppen källkod för att hålla systemet lätt och skalbart. CI/CD‑pipelines, automatiserad provisionering av infrastruktur och övervakningsverktyg implementerades också för att säkerställa tillförlitligheten. När teamet växte fick nya ingenjörer mentorskap, det tvärfunktionella samarbetet underlättades och användarnas återkoppling prioriterades för att förfina applikationen iterativt. Med tiden utvecklades GIS‑verktyget från en enkel prototyp till en sofistikerad plattform, med avancerad analys och anpassade dashboards för att möta kundernas krav.</p>\n<p><strong>Resultat.</strong> GIS‑applikationen blev en hörnsten i SaaS‑plattformen och låg bakom att intäkterna blev 10 gånger högre genom merförsäljning, datalicensiering och värvningen av nya kunder. Förmågan att visualisera anläggningar för grön energi i realtid förbättrade den operativa effektiviteten hos kunderna, samtidigt som den ständiga förfiningen av verktyget gjorde det till en primär datatillgång. Projektets framgång befäste inte bara företagets rykte inom sektorn för förnybar energi utan visade också värdet av ett tvärdisciplinärt arbetssätt i en snabbrörlig startupmiljö. Genom att teknisk expertis kombinerades med strategisk vision blev GIS‑applikationen en avgörande konkurrensfördel som drev långsiktig tillväxt och innovation.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "DevOps", "Fullstackutveckling", "GIS / geodata", "Lösningsarkitektur", "Mentorskap och coachning", "Plattformsarkitektur", "Produkt och krav", "Teamledning", "Tekniskt ledarskap", "Tillförlitlighet och säkerhetskopiering", "Fullstackutveckling av produkter", "GIS- och geodatalösningar", "Plattforms- och lösningsarkitektur", "Produktstrategi och krav", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/", "url": "https://engineer.company/sv/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/", "title": "Styrde full stack‑utvecklingen av GIS‑kartor med ansvar för PostgreSQL, Mapbox, ReactJS och NodeJS för att leverera en integrerad lösning.", "summary": "Ledde fullstackutvecklingen av GIS-kartor (PostgreSQL, Mapbox, React, Node.js) och levererade en integrerad, utbyggbar kärna i plattformen.", "content_html": "<p><strong>Situation.</strong> Vid det här laget hade GIS‑kartan slutat vara en funktion och blivit anledningen till att kunderna loggade in. Problemet var att den hade vuxit fram i bitar. De spatiala data låg i PostgreSQL, själva kartan ritades med Mapbox, och applikationen runt omkring var ReactJS i frontend med NodeJS bakom. Varje del fungerade för sig. De hade bara inte byggts för att passa ihop, och skarvarna började synas.</p>\n<p><strong>Uppgift.</strong> Rollen omfattade fullstackutvecklingen av kartan och den tekniska riktning som följde med den: datamodellen, renderingen, API:et och frontenden i React. Målet var att förvandla fyra saker som råkade ligga i samma repo till en enda produkt värd att stå för.</p>\n<p><strong>Handling.</strong> Arkitekturen lades fast först, och sedan höll sig arbetet nära koden i stället för att styra på avstånd. På datasidan hölls den spatiala modellen i PostgreSQL ren, så att databasfrågorna inte gick ned i snigelfart när dataseten växte. Mapbox stod för ritandet; jobbet var att mata det med rätt data på rätt zoomnivåer i stället för allt på en gång. På applikationssidan granskades arbetet i ReactJS och NodeJS, teamet knuffades mot gemensamma konventioner och ansvar flyttades gång på gång tillbaka till det lager där det hörde hemma så fort ett lager började läcka in i nästa. En hel del av det var oglamoröst arbete: att fånga de små inkonsekvenserna innan de stelnade till arkitektur.</p>\n<p><strong>Resultat.</strong> Resultatet blev en enda integrerad kartapplikation, där data, rendering och gränssnitt äntligen drog åt samma håll. Den blev en central del av plattformen, och något som teamet kunde fortsätta bygga ut utan att den gav vika varje gång ett nytt lager lades till.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Databaser", "Frontendutveckling", "Fullstackutveckling", "GIS / geodata", "Plattformsarkitektur", "PostgreSQL", "Prestandaoptimering", "Tekniskt ledarskap", "Backend- och API‑utveckling", "Fullstackutveckling av produkter", "GIS- och geodatalösningar", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/", "url": "https://engineer.company/sv/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/", "title": "Sparade 4 000 timmar genom att handleda och bygga ut ett team från 2 till 18 medlemmar, optimera arbetsflöden och främja samarbete mellan avdelningar.", "summary": "Sparade ~4 000 timmar genom att utöka och handleda ett team från 2 till 18, omforma arbetsflöden och bygga varaktig ingenjörsförmåga.", "content_html": "<p><strong>Situation.</strong> Ett team på två hann inte med längre. Produkten drog in mer arbete än ett par kunde leverera, och sättet arbetet gick till på — kunskap i folks huvuden, inga riktiga konventioner — skulle inte överleva att skalas upp. Att fylla på med folk i ett så löst organiserat team gör oftast bara kaoset större.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att få teamet att växa och samtidigt bygga den struktur som skulle låta en större grupp röra sig snabbare i stället för långsammare. Mentorskapet för de nya var ena halvan av det. Att rätta till arbetsflödena så att ingen fastnade i väntan på någon annan var den andra halvan.</p>\n<p><strong>Handling.</strong> Teamet växte med tiden från 2 till 18, och rekryteringen och mentorskapet behandlades som samma jobb: alla som kom in behövde kunna arbeta utan uppsikt. Gemensamma standarder gjorde att kod och process såg likadana ut oavsett vem som hade skrivit dem, och rejäl möda lades ned på överlämningarna mellan specialistområdena, eftersom det är där team i det tysta förlorar sina dagar. När något gång på gång fick folk att snubbla riktades åtgärden mot processen i stället för mot symtomet.</p>\n<p><strong>Resultat.</strong> Det större och bättre handledda teamet, som arbetade efter arbetsflöden som faktiskt hade utformats, sparade i storleksordningen 4 000 timmar. Men siffran är egentligen inte poängen. Det som byggdes var en varaktig ingenjörsförmåga — en grupp som kunde bära arbetet med eller utan någon enskild person i rummet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Mentorskap och coachning", "Projektledning", "Teamledning", "Tekniskt ledarskap", "Projektledning (agil)", "Teambygge och mentorskap", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/", "url": "https://engineer.company/sv/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/", "title": "Optimerade prognos- och investeringsstrategier för 11 elnätsföretag och drev på driftseffektiviteten med datadrivna GIS‑lösningar.", "summary": "Optimerade prognoser och investeringsstrategi för 11 elnätsbolag med datadriven GIS — pålitlig, målinriktad planering.", "content_html": "<p><strong>Situation.</strong> Elnätsbolag står och faller med besluten om var nätet ska förstärkas och var pengarna ska läggas, och elva av dem fattade de besluten utan någon större geospatial analys som grund. De hade driftdata. Det de saknade var ett sätt att se dem på kartan, där mönstren faktiskt finns.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att skärpa deras prognoser och investeringsstrategier med GIS — att förvandla tabeller med mätvärden till något som visade dem var kapaciteten började bli trång, var riskerna byggdes upp och var nästa pund gjorde mest nytta.</p>\n<p><strong>Handling.</strong> Deras driftdata fördes samman med geospatial modellering så att de två förstärkte varandra. I stället för att prognoser gjordes i det abstrakta kunde nätet studeras spatialt och konkreta frågor ställas: vilka ledningssträckor var på väg mot sina gränser, vilka områden motiverade investeringar först. För elva nätbolag innebar det att anpassa analysen efter hur vart och ett av dem faktiskt drev sitt nät, inte att ge alla samma mall och hoppas att den passade.</p>\n<p><strong>Resultat.</strong> Nätbolagen fick med sig prognoser de kunde lita på och investeringsbeslut som var välriktade snarare än förhoppningsfulla. Att grunda planeringen i det kartan visade gjorde det hela effektivare — pengar och uppmärksamhet gick dit data pekade i stället för dit vanan gjorde.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Dataanalys", "GIS / geodata", "Intressenter och rapportering", "Produkt och krav", "Dataanalys och BI‑dashboards", "GIS- och geodatalösningar", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/", "url": "https://engineer.company/sv/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/", "title": "Presenterade 200 UI/UX‑förbättringar för GIS‑kartapplikationen och ökade intäkterna 10 gånger genom förbättrade programfunktioner.", "summary": "Levererade 200 UI/UX-förbättringar i en GIS-kartapplikation som bidrog till 10 gånger högre intäkter — genomtänkt UX som kommersiell drivkraft.", "content_html": "<p><strong>Situation.</strong> Kartgränssnittet hade blivit kraftfullt och på vägen också komplicerat. På sina ställen kunde man känna att kunderna inte fick ut värdet som fanns precis framför dem — bra funktionalitet som satt fast bakom klumpiga interaktioner. Det glappet mellan vad produkten kunde göra och vad användarna tyckte var lätt att göra blev i det tysta en kostnad för företaget.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att hitta de glappen och driva igenom åtgärderna: det användbarhetsarbete som skulle göra det lättare att få ut värde av produkten och, inte av en slump, göra den mer värdefull kommersiellt.</p>\n<p><strong>Handling.</strong> I stället för att gissa vad som var fel gick arbetet ut på att gräva i återkopplingen och användningsdatan, och ur det växte en backlog med 200 konkreta UI/UX‑förbättringar fram. De behandlades inte som likvärdiga — de rangordnades efter effekt, och de som verkligen spelade roll förespråkades och drevs genom teamet tills de levererades som riktiga funktioner, i stället för att bli en önskelista som blev liggande i ett dokument och föråldrades.</p>\n<p><strong>Resultat.</strong> Gränssnittet blev märkbart bättre att använda, och produktens värde följde efter: arbetet bidrog till en tiodubbling av intäkterna. Det är ett fall värt att återkomma till, eftersom det visar poängen så tydligt — omsorgsfull UX är inte kosmetika, den syns på fakturan.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dataanalys", "Designsystem och UI", "Frontendutveckling", "Intressenter och rapportering", "Produkt och krav", "UX/UI‑design", "Produktstrategi och krav", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/designed-and-managed-5-000-hours-of-map-29/", "url": "https://engineer.company/sv/portfolio/designed-and-managed-5-000-hours-of-map-29/", "title": "Utformade och ledde 5 000 timmars kartutveckling med agila metoder som Scrum och Jira för effektiv projektledning.", "summary": "Planerade och drev 5 000 timmars kartutveckling med Scrum och Jira och höll arbetet synligt och i linje med färdplanen.", "content_html": "<p><strong>Situation.</strong> Kartan byggdes inte under en sprint. Det var tusentals timmars arbete fördelat på många personer och många månader, och den sortens insats driver iväg om ingen håller fast vid omfattningen och tidsplanen. Lämnas den åt sig själv halkar den efter i det tysta.</p>\n<p><strong>Uppgift.</strong> Ansvaret var att utforma och driva utvecklingsarbetet så att det levererade med avsikt snarare än av tur — att hålla prioriteringarna ärliga och framstegen synliga för alla som ville titta.</p>\n<p><strong>Handling.</strong> Utvecklingen drevs agilt: ordentlig Scrum, där ceremonierna faktiskt användes snarare än spelades upp, och arbetet följdes i Jira så att alla kunde se läget utan att behöva fråga. Någonstans runt 5 000 timmar kartutveckling gick genom den processen. Tyngdpunkten låg mindre på ceremonier för deras egen skull och mer på att hålla prioriteringarna riktade mot det som var viktigt och att fånga avvikelser tidigt, medan de fortfarande var billiga att rätta till.</p>\n<p><strong>Resultat.</strong> De 5 000 timmarna landade på ett kontrollerat och synligt sätt i stället för att försvinna in i en svart låda. Projektledningen gjorde det den är till för — och det som oftast går obemärkt förbi när den fungerar: den höll arbetet i linje med målen och ungefär enligt tidsplanen, utan hjältedåd i slutet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Intressenter och rapportering", "Produkt och krav", "Projektledning", "Teamledning", "Produktstrategi och krav", "Projektledning (agil)", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/", "url": "https://engineer.company/sv/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/", "title": "Skrev 50 000 ord utförlig programvarudokumentation i Markdown på GitHub, Craft och Confluence och säkerställde därmed att kunskapen bevarades och att processen var transparent.", "summary": "Skrev 50 000 ord mjukvarudokumentation i GitHub, Craft och Confluence och gjorde kunskapen varaktig, transparent och granskningsbar.", "content_html": "<p><strong>Situation.</strong> Det mesta som var viktigt om produkten och om hur teamet arbetade fanns i folks huvuden. Det går bra ända tills någon ny börjar, eller någon slutar — och då kryper onboardingen fram och en del av det institutionella minnet är bara en uppsägning från att försvinna för gott.</p>\n<p><strong>Uppgift.</strong> Målet var att få ut den kunskapen ur huvudena och in i dokumentation som folk faktiskt skulle hålla vid liv och använda: tydlig nog att läsas, strukturerad nog att underhållas i stället för att överges efter en månad.</p>\n<p><strong>Handling.</strong> Mycket av den skrevs för hand, runt 50 000 ord till slut, i Markdown i GitHub, Craft och Confluence beroende på var varje del hörde hemma. Den täckte arkitekturen, processerna och det praktiska instruktionsmaterialet — frågorna som folk ställde om och om igen. Den hölls versionshanterad och, lika viktigt, lätt att hitta, eftersom dokumentation som ingen kan hitta lika gärna kunde vara oskriven.</p>\n<p><strong>Resultat.</strong> Kunskapen slutade vara bräcklig. Nya medarbetare kom in i arbetet snabbare, processen blev något man kunde peka på i stället för att rekonstruera ur minnet, och helheten blev granskningsbar: man kunde se hur och varför arbetet gjordes i stället för att ta det på tro.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dokumentation", "Mentorskap och coachning", "Teamledning", "Tekniskt ledarskap", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/", "url": "https://engineer.company/sv/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/", "title": "Samlade in och analyserade verksamhetskrav och omsatte dem i genomförbara funktioner och user stories i linje med standarderna för datastyrning.", "summary": "Omsatte verksamhetskrav i tydliga funktioner och user stories i linje med standarderna för datastyrning, vilket minskade oklarheter och omarbete.", "content_html": "<p><strong>Situation.</strong> Kraven kom oftast i form av samtal — någon ville ha något, ungefär, och det föll på utvecklarna att gissa sig till avgränsningarna. Gissningar leder till omarbete, och omarbete är ungefär det dyraste sätt som finns att bygga något på.</p>\n<p><strong>Uppgift.</strong> Rollen låg mellan verksamheten och tekniken och översatte det ena till det andra: att göra lösa behov till arbete som en utvecklare kunde ta sig an utan att gissa, och att samtidigt hålla det i linje med standarderna för datastyrning.</p>\n<p><strong>Handling.</strong> Kraven arbetades fram direkt med intressenterna, där de besvärliga frågorna ställdes tidigt i stället för att upptäckas sent, och skrevs sedan ned som funktioner och user stories som faktiskt sa vad ”klar” betydde. Var och en stämdes av mot styrningsreglerna, eftersom en funktion som är användbar men hanterar data fel inte riktigt är färdig. Hela poängen var att någon skulle kunna läsa en story och bygga rätt sak på första försöket.</p>\n<p><strong>Resultat.</strong> Utvecklingen utgick från tydliga, överenskomna funktioner i stället för halvt förstådda önskemål. Otydligheten minskade, omarbetet minskade med den, och leveransen förblev riktad mot de faktiska affärsmålen — inom ramarna för datastyrningen i stället för att snyggas till i efterhand för att passa in.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Datastyrning", "Dokumentation", "Intressenter och rapportering", "Produkt och krav", "Projektledning", "Datastyrning och datakvalitet", "Produktstrategi och krav", "Projektledning (agil)" ] }, { "id": "https://engineer.company/sv/portfolio/managed-the-execution-of-30-successful-gis-projects-32/", "url": "https://engineer.company/sv/portfolio/managed-the-execution-of-30-successful-gis-projects-32/", "title": "Ledde genomförandet av 30 framgångsrika GIS‑projekt och visade ledarskap genom att leverera innovativa lösningar inom hela området.", "summary": "Levererade 30 framgångsrika GIS-projekt — en historik av jämna, innovativa leveranser som gav varaktigt förtroende hos kunderna.", "content_html": "<p><strong>Situation.</strong> Företaget tjänade sina pengar på att leverera GIS‑projekt, och många av dem pågick samtidigt. Företagets tillväxt hängde på att få ut dem på ett tillförlitligt sätt — inte ett enda flaggskeppsprojekt som utfördes briljant, utan en jämn ström av projekt som landade i tid och höll ihop, vilket bara händer när samordningen i teamet faktiskt fungerar.</p>\n<p><strong>Uppgift.</strong> Ansvaret var att få hela den portföljen levererad — att hålla omfattning, människor och tidsplaner i linje genom alltihop, och att ge teamet riktningen för att leverera.</p>\n<p><strong>Handling.</strong> Under de åren gick leveransen av trettio GIS‑projekt genom den här rollen. I praktiken innebar det att hålla omfattningen stabil när den ville glida iväg, att rikta rätt personer mot rätt arbete och att ligga tillräckligt nära varje projekt för att fånga problem medan de fortfarande var små. När något skulle komma att bli försenat var det bättre att veta tidigt och planera om än att upptäcka det vid deadline. En stor del av jobbet handlade helt enkelt om att hålla alla bollar i luften och hålla kundens förväntningar ärliga om vad som skulle komma och när.</p>\n<p><strong>Resultat.</strong> Alla trettio levererades. Den jämnheten betydde mer än något enskilt projekt hade kunnat göra — en kund som har sett leveranserna komma trettio gånger om oroar sig inte för den trettioförsta, och det ryktet är en god del av förklaringen till att företaget fortsatte att växa.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "GIS / geodata", "Intressenter och rapportering", "Projektledning", "Teamledning", "GIS- och geodatalösningar", "Projektledning (agil)", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/led-company-growth-from-4-to-14-employees-33/", "url": "https://engineer.company/sv/portfolio/led-company-growth-from-4-to-14-employees-33/", "title": "Ledde företagets tillväxt från 4 till 14 anställda med agila metoder och effektiv projektledning.", "summary": "Ledde företagets tillväxt från 4 till 14 anställda med agila arbetssätt och skalade teamet utan att tumma på kvalitet eller sammanhållning.", "content_html": "<p><strong>Situation.</strong> Företaget var redo att växa, och just det ögonblicket bär på en särskild risk: man tar in människor snabbare än man tar in processer, och både kvaliteten och den gemensamma bilden av hur saker görs börjar luckras upp. Fyra personer som alla vet vad alla andra gör är något helt annat än fjorton som inte vet det.</p>\n<p><strong>Uppgift.</strong> Jobbet var att leda den tillväxten — att ta in nya människor och samtidigt hålla leveransen disciplinerad och få teamet att dra åt samma håll.</p>\n<p><strong>Handling.</strong> Företaget gick från fyra personer till fjorton. Det handlade om rekrytering och onboarding som gjordes med eftertanke snarare än i panik, men den större delen var att införa de arbetssätt som gjorde att ett team av den storleken inte snubblade över sina egna fötter — agila metoder, riktig projektledning, de vanor som håller allas arbete synligt för alla andra. Den tionde och den fjortonde nyanställda behövde komma in i något som redan hade en form, inte behöva gissa sig till hur saker gjordes.</p>\n<p><strong>Resultat.</strong> Teamet växte till fjorton utan att kvaliteten sjönk eller att det splittrades i personer som inte visste vad de andra höll på med. Det var den agila delen som höll en större grupp produktiv — processen växte i takt med antalet anställda i stället för att släpa efter.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Mentorskap och coachning", "Projektledning", "Teamledning", "Tekniskt ledarskap", "Projektledning (agil)", "Teambygge och mentorskap", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/", "url": "https://engineer.company/sv/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/", "title": "Förbättrade teamets kommunikation och samarbete genom att införa Slack, Mattermost, 1Password och Jira, vilket sparade 8 000 arbetstimmar.", "summary": "Sparade ~8 000 timmar genom att införa Slack, Mattermost, 1Password och Jira och förvandlade jakt på information och omarbete till verklig leverans.", "content_html": "<p><strong>Situation.</strong> När teamet växte hade kommunikationen och verktygen inte hängt med, och det märktes. Saker sades på ett ställe och missades av dem som behövde höra dem, arbete gjordes dubbelt eftersom ingen kunde se vad någon annan redan hade gjort, och att samordna något tog längre tid än själva arbetet. Den sortens friktion är osynlig från dag till dag, men den blir till mycket förlorad tid.</p>\n<p><strong>Uppgift.</strong> Målet var att få ordning på hur teamet kommunicerade och samarbetade, och att ta tillbaka den tid som i det tysta läckte bort i all den friktionen.</p>\n<p><strong>Handling.</strong> Verktygen infördes och standardiserades, och — det här är den del som faktiskt spelar roll — rutinerna för hur de skulle användas fastställdes, så att de inte bara blev ytterligare ett ställe att kolla. Slack och Mattermost för kommunikation, 1Password så att delade hemligheter inte skickades runt på sätt som ingen kunde spåra, Jira så att arbetet följdes upp på ett ställe i stället för att leva i folks huvuden och inkorgar. Verktygen var den enkla biten; att få alla att faktiskt använda dem på samma sätt var det verkliga arbetet.</p>\n<p><strong>Resultat.</strong> Kommunikationen och samarbetet blev märkbart bättre, och den strömlinjeformade uppsättningen sparade något i storleksordningen 8 000 arbetstimmar — tid som tidigare hade gått åt till att jaga information och göra om arbete, och som nu gick till faktisk leverans.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Automatisering och CI/CD", "Dokumentation", "Projektledning", "Säkerhet", "Teamledning", "Projektledning (agil)", "Säkerhet och behörighetshantering", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/led-the-development-deployment-and-support-of-over-35/", "url": "https://engineer.company/sv/portfolio/led-the-development-deployment-and-support-of-over-35/", "title": "Ledde utveckling, driftsättning och support av över 30 GIS‑projekt och visade expertis inom PostgreSQL, Bash, Python, JavaScript, GDAL, ArcGIS, PostGIS och Mapbox.", "summary": "Ledde utveckling, driftsättning och support av 30+ GIS-projekt med PostgreSQL, Python, GDAL, ArcGIS, PostGIS och Mapbox genom hela livscykeln.", "content_html": "<p><strong>Situation.</strong> Allt företaget levererade var skräddarsydda GIS‑lösningar — specialbyggda kart- och geodatasystem för en kunds specifika problem, som sedan hölls igång när de väl hade tagits i drift. Att bygga dem är bara halva jobbet; ett geodataprojekt som levereras och sedan faller ihop i produktion har inte riktigt levererats.</p>\n<p><strong>Uppgift.</strong> De här projekten gick från början till slut genom den här rollen — utvecklingen, driftsättningen och supporten när de väl var i drift — med den tekniska styrningen över en ganska bred stack.</p>\n<p><strong>Handling.</strong> Leveransen omfattade mer än trettio GIS‑projekt, med praktiskt arbete i hela stacken hela vägen. I botten låg PostgreSQL med PostGIS för geodatan, GDAL/OGR för att flytta den mellan format — shapefiler, GeoJSON, GeoTIFF, KML, vektortiles — och QGIS och JOSM för själva dataarbetet. På frontendsidan byggdes webbkartorna på Mapbox GL och Leaflet, ibland mot ArcGIS- eller HERE‑API:erna, med applikationslagret i JavaScript, Python, PHP och SQL. Arbetet spände från digital kartläggning i 2D och 3D via LiDAR‑bearbetning, georeferering och vektorisering till inomhuskartläggning och navigering. Och ansvaret sträckte sig längre än till leveransen — driftsättningen och den löpande supporten i produktion ingick också, så problemen lämnades inte över till någon annan; besluten fick man leva med själv.</p>\n<p><strong>Resultat.</strong> Fler än trettio projekt byggda, driftsatta och med support i produktion, över hela det spannet. Att bära ansvaret för hela livscykeln och inte bara för bygget är det som höll kvaliteten ärlig — man designar annorlunda när man vet att det är en själv som blir uppringd om något går sönder.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Data engineering", "Databaser", "Fullstackutveckling", "GIS / geodata", "PostgreSQL", "Python", "SQL", "Teamledning", "Tekniskt ledarskap", "Tillförlitlighet och säkerhetskopiering", "Webbutveckling", "Backend- och API‑utveckling", "Fullstackutveckling av produkter", "GIS- och geodatalösningar", "Tekniskt ledarskap och rådgivning", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/contributed-to-user-interface-design-processes-ensuring-intuitive-36/", "url": "https://engineer.company/sv/portfolio/contributed-to-user-interface-design-processes-ensuring-intuitive-36/", "title": "Bidrog till designprocesserna för användargränssnitt och säkerställde intuitiva, visuellt tilltalande och användarvänliga gränssnitt i projekten.", "summary": "Bidrog till UI-designen med intuitiva, polerade och användarvänliga gränssnitt genom att ställa frågor om användbarhet under designarbetet.", "content_html": "<p><strong>Situation.</strong> Gränssnitten i projekten var ojämna. En del fungerade bra, en del hade uppenbart byggts av ingenjörer som tänkte på datamodellen snarare än på personen som skulle använda dem, och designbesluten fattades inte alltid med slutanvändaren i rummet. Särskilt i en webbkartprodukt är kartan den enkla delen — det är i kontrollerna, filtreringen och flödet runt den som folk går vilse.</p>\n<p><strong>Uppgift.</strong> Att engagera sig i UI‑designprocessen var en del av rollen — att bidra till att göra gränssnitten till något som folk faktiskt upplevde som intuitivt och trevligt att använda, inte bara funktionellt.</p>\n<p><strong>Handling.</strong> Rollen var inte designerns, men den fanns med i designprocessen och bidrog med ett ingenjörsperspektiv — den ifrågasatte layouten, flödet genom en uppgift och om en skärm faktiskt var tydlig eller bara välbekant för dem som hade byggt den. Det här var frontends i React, Angular och Vue ovanpå kartor i Mapbox och Leaflet, och mycket av användbarheten satt i detaljerna: hur man filtrerade en datamängd, hur man rörde sig mellan våningsplan på en inomhuskarta, om gränssnittet talade om vad det höll på med. För det mesta innebar det att ställa de naiva användarfrågorna tidigt, medan de fortfarande var billiga att åtgärda, i stället för efter lanseringen när förvirringen kom tillbaka som supportärenden.</p>\n<p><strong>Resultat.</strong> Gränssnitten blev mer intuitiva och mer genomarbetade, vilket slutanvändarna märkte direkt, och det lyfte den övergripande kvaliteten på det som släpptes. Att användbarhetsfrågorna ställdes under designen snarare än efter lanseringen är det mesta av det som gjorde skillnaden.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Frontendutveckling", "GIS / geodata", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/", "url": "https://engineer.company/sv/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/", "title": "Vägledde genomförandet av ett stort antal projekt i företaget och gav expertstöd i mjukvaruutvecklingens olika faser.", "summary": "Vägledde genomförandet av många av företagets projekt med expertstöd genom de svåraste faserna i mjukvaruutvecklingen och stabiliserade leveranserna.", "content_html": "<p><strong>Situation.</strong> Många projekt pågick samtidigt, vart och ett mitt i sin utvecklingsfas, och alla behövde stadig teknisk vägledning för att hålla sig på rätt spår. Lämnas projekt åt sig själva driver de iväg — ett felaktigt angreppssätt som väljs tidigt hinner bli dyrt innan någon märker det.</p>\n<p><strong>Uppgift.</strong> Jobbet var att vägleda det genomförandet — att vara det tekniska stöd som teamen kunde luta sig mot genom utvecklingsfaserna.</p>\n<p><strong>Handling.</strong> Arbetet förblev praktiskt och handfast i många av företagets projekt samtidigt. Det innebar att hjälpa folk vidare när de satt fast, att granska ett angreppssätt noga innan för mycket hade byggts ovanpå det, och överlag att ligga tillräckligt nära för att upptäcka ett projekt som var på väg åt fel håll medan det fortfarande handlade om en kurskorrigering och inte om att bygga om. Tanken var att vara tillgänglig snarare än en grindvakt — att hålla saker i rörelse, inte att låta allt vänta på en enda person.</p>\n<p><strong>Resultat.</strong> Projekten tog sig smidigare igenom sina knepiga utvecklingsskeden med någon erfaren att luta sig mot i rätt ögonblick, och det gjorde leveranserna stabilare i hela företagets portfölj.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Mentorskap och coachning", "Projektledning", "Teamledning", "Tekniskt ledarskap", "Projektledning (agil)", "Teambygge och mentorskap", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/", "url": "https://engineer.company/sv/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/", "title": "Gjorde en genomgripande översyn av de interna processerna och sparade 8 000 timmar genom att förbättra mjukvaruarkitekturen, systemen och effektiviteten i schemaläggningen.", "summary": "Omarbetade interna processer och sparade ~8 000 timmar genom effektivare mjukvaruarkitektur, system och schemaläggning.", "content_html": "<p><strong>Situation.</strong> Det interna arbetssättet hade samlat på sig den vanliga bråten — en mjukvaruarkitektur som hade vuxit genom pålagringar snarare än design, system som fungerade men inte effektivt, schemaläggning som gjorde att folk antingen väntade eller var överbelastade. Inget av det brann i knutarna, vilket är precis varför det hade lämnats i fred, men det kostade i det tysta mycket tid.</p>\n<p><strong>Uppgift.</strong> Målet var att se över de processerna i grunden — att leta upp slöseriet och ta bort det i stället för att fortsätta betala för det.</p>\n<p><strong>Handling.</strong> De interna processerna arbetades om på tre fronter: mjukvaruarkitekturen, så att den blev något man kunde resonera kring och bygga vidare på i stället för att arbeta runt; systemen, effektiviserade så att rutinarbetet slutade ta längre tid än det borde; och schemaläggningen, så att kapaciteten faktiskt matchades mot arbetet. Och förändringarna gjordes bestående — inbäddade i hur teamet arbetade i stället för att lämnas som ett pm som alla nickade åt och glömde — eftersom processförbättringar som inte görs permanenta bara glider tillbaka till det gamla sättet.</p>\n<p><strong>Resultat.</strong> Översynen sparade ungefär 8 000 timmar genom att göra arkitekturen, systemen och schemaläggningen påtagligt effektivare. Det är kapacitet som gick direkt tillbaka till mer värdeskapande arbete i stället för till merarbete som ingen hade tänkt på att ifrågasätta.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Lösningsarkitektur", "Plattformsarkitektur", "Prestandaoptimering", "Projektledning", "Tekniskt ledarskap", "DevOps och CI/CD‑automatisering", "Plattforms- och lösningsarkitektur", "Projektledning (agil)", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/administered-network-infrastructure-for-over-1-000-servers-39/", "url": "https://engineer.company/sv/portfolio/administered-network-infrastructure-for-over-1-000-servers-39/", "title": "Administrerade nätverksinfrastrukturen för över 1 000 servrar och säkerställde optimal driftsättning, säkerhet och felsökning av systemen.", "summary": "Administrerade nätverksinfrastruktur för 1 000+ servrar och säkerställde tillförlitlig driftsättning, stark säkerhet och snabb felsökning.", "content_html": "<p><strong>Situation.</strong> Företagets verksamhet vilade på en stor serverpark — över tusen servrar — och en park av den storleken förblir inte tillförlitlig av sig själv. Driftsättning, säkerhet och den jämna strömmen av saker som går fel behöver alla någon som sköter dem med verklig disciplin, annars blir det hela instabilt och ingen vet riktigt varför.</p>\n<p><strong>Uppgift.</strong> Jobbet var att administrera den nätverksinfrastrukturen — att hålla den säker, hålla den tillförlitlig och hålla den enhetlig i en skala där det är bristen på enhetlighet som sänker allt.</p>\n<p><strong>Handling.</strong> Nätverksinfrastrukturen omfattade mer än tusen servrar. Sättet som system driftsattes på standardiserades, så att en server kom upp på samma förutsägbara sätt i stället för att var och en var lite specialanpassad; säkerheten härdades i stället för att man litade på att ingen skulle komma och leta; och felsökningen sköttes när något väl gick sönder. I den skalan är det standardiseringen som räddar situationen — tusen unika snöflingor går inte att hantera, tusen exemplar av samma sak är bara arbete.</p>\n<p><strong>Resultat.</strong> Serverparken drevs med tillförlitlig driftsättning, gedigen säkerhet och problem som åtgärdades snabbt i stället för att få ligga och gro. Det är den sortens infrastrukturarbete som är osynligt när det går bra, och det är hela poängen — det var den stabila ryggrad som allt annat i företaget vilade på.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Systemadministration", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Nätverk och VPN‑uppsättning", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/established-a-technical-support-department-servicing-over-10-40/", "url": "https://engineer.company/sv/portfolio/established-a-technical-support-department-servicing-over-10-40/", "title": "Etablerade en avdelning för teknisk support som betjänade över 10 000 kunder med IT‑support och felsökning och följde upp kundärenden i ett CRM‑system.", "summary": "Etablerade en avdelning för teknisk support med 10 000+ kunder och förvandlade supporten från reaktiva åtgärder till en skalbar styrka för företaget.", "content_html": "<p><strong>Situation.</strong> Kundbasen växte och behövde pålitlig teknisk support, och det fanns helt enkelt ingen dedikerad funktion som kunde ge den i någon verklig skala. Supporten sköttes ad hoc, vilket fungerar för en handfull kunder och i det tysta faller samman när antalet stiger.</p>\n<p><strong>Uppgift.</strong> Att bygga upp en riktig förmåga till teknisk support — en som kunde betjäna en stor och fortfarande växande kundbas — var jobbet.</p>\n<p><strong>Handling.</strong> En avdelning för teknisk support byggdes upp från ingenting. Det innebar att bestämma hur den faktiskt skulle fungera innan någon anställdes till den — processen för hur ett ärende kom in och löstes, verktygen för att hantera volymen, standarden för hur bra support såg ut — och sedan bygga upp kapaciteten att leverera IT‑support och felsökning till många kunder samtidigt. Att börja från noll var ärligt talat fördelen; avdelningen kunde utformas för den skala som företaget var på väg mot i stället för att lappa på något som hade vuxit fram av en slump.</p>\n<p><strong>Resultat.</strong> Avdelningen kom till slut att betjäna över tiotusen kunder med pålitlig support och felsökning. Supporten gick från att vara något som gjordes reaktivt till en verklig styrka för företaget — något som växte i takt med kundbasen i stället för att knäckas under den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Intressenter och rapportering", "Mentorskap och coachning", "Produkt och krav", "Projektledning", "Systemadministration", "Teamledning", "IT‑support och helpdesk", "Projektledning (agil)", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/automated-ssl-tls-certificate-creation-for-100-docker-41/", "url": "https://engineer.company/sv/portfolio/automated-ssl-tls-certificate-creation-for-100-docker-41/", "title": "Automatiserade skapandet av SSL/TLS‑certifikat för 100 Docker‑applikationer och säkerställde säkra anslutningar på alla värdar med Ubuntu Linux.", "summary": "Automatiserade SSL/TLS-certifikat för 100 Docker-appar på Ubuntu Linux och avskaffade manuella förnyelser och avbrotten som utgångna certifikat orsakar.", "content_html": "<p><strong>Situation.</strong> Hundra Docker‑applikationer behövde alla SSL/TLS‑certifikat, och certifikat är sådant som fungerar ända tills de inte gör det. Att utfärda och förnya hundra av dem för hand går långsamt, det är tråkigt, och det är precis den sortens manuella jobb där en enda bortglömd förnyelse slår ut en applikation med ett utgångsfel vid sämsta tänkbara tillfälle.</p>\n<p><strong>Uppgift.</strong> Målet var att få skapandet och förnyelsen av certifikat automatiserade — varje applikation med giltig, betrodd kryptering, utan att någon behövde komma ihåg att göra något.</p>\n<p><strong>Handling.</strong> Ett ACME‑baserat arbetsflöde skötte hela livscykeln för de hundra Docker‑applikationerna — skapade certifikaten och förnyade dem innan de gick ut — och driftsatte dem automatiskt på värdarna med Ubuntu Linux, som körde en blandning av Apache och Nginx. Hela poängen var att ta bort människan ur kedjan, eftersom det är människan som glömmer.</p>\n<p><strong>Resultat.</strong> Alla hundra applikationer behöll giltiga certifikat och säkra anslutningar av sig själva. Det manuella certifikatarbetet försvann helt enkelt, och med det en hel kategori av avbrott där något går sönder inte för att det har fallerat, utan för att ett certifikat i det tysta har gått ut utan att någon märkt det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "DevOps", "Infrastruktur", "Linux och servrar", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Containerisering och orkestrering", "DevOps och CI/CD‑automatisering", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/", "url": "https://engineer.company/sv/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/", "title": "Effektiviserade CI/CD‑processerna och sparade 4 000 timmar genom att införa automatisering i pipelines för mjukvaruutveckling.", "summary": "Effektiviserade CI/CD och sparade ~4 000 timmar med snabbare och mer tillförlitliga releaser, så att teamet kunde leverera med självförtroende.", "content_html": "<p><strong>Situation.</strong> Att få ut mjukvara byggde på manuella, inkonsekventa steg — någon som kom ihåg ordningsföljden och gjorde lite olika varje gång — och det bromsade releaserna och åt upp ingenjörstimmar som borde ha gått till att bygga saker.</p>\n<p><strong>Uppgift.</strong> Målet var att effektivisera CI/CD‑processen och få in automatisering i pipelines, så att releaser slutade vara en manuell ritual.</p>\n<p><strong>Handling.</strong> Automatiserade pipelines för bygge, test och driftsättning infördes, så att vägen från en ändring till att den körde i produktion blev standardiserad i stället för improviserad. De repetitiva manuella stegen — de som var långsamma och, ännu värre, gjordes olika beroende på vem som gjorde dem — togs bort. När pipelinen gör det på samma sätt varje gång slutar en hel kategori av ”det funkade på min dator” och halvt ihågkomna driftsättningssteg helt enkelt att inträffa.</p>\n<p><strong>Resultat.</strong> Automatiseringen gav tillbaka ungefär 4 000 timmar och gjorde releaserna både snabbare och mer tillförlitliga. Teamet kunde leverera utan att behöva förbereda sig på det värsta — tryggheten kom av att processen var konsekvent, inte av att alla var försiktiga.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Tekniskt ledarskap", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/streamlined-data-analysis-and-software-development-processes-saving-43/", "url": "https://engineer.company/sv/portfolio/streamlined-data-analysis-and-software-development-processes-saving-43/", "title": "Effektiviserade processerna för dataanalys och mjukvaruutveckling och sparade 4 000 timmar genom att införa CI/CD‑arbetssätt med GitHub, GitLab, Bash och Python.", "summary": "Sparade ~4 000 timmar genom att införa CI/CD med GitHub, GitLab, Bash och Python, vilket snabbade upp dataanalys och utveckling och gav ökad konsekvens.", "content_html": "<p><strong>Situation.</strong> Både dataanalysarbetet och mjukvaruutvecklingen hölls tillbaka av samma sak: manuella processer. Arbetet rörde sig från utveckling till leverans på ett långsamt och inkonsekvent sätt, och analyssidan hade sin egen hög av repetitiva steg som någon gjorde för hand varje gång.</p>\n<p><strong>Uppgift.</strong> Målet var att effektivisera båda genom att föra in modern automatisering och CI/CD‑praxis i arbetsflöden som inte hade haft det.</p>\n<p><strong>Handling.</strong> CI/CD‑praxis infördes, byggd på GitHub och GitLab, med Bash och Python som skötte automatiseringen under ytan. De repetitiva stegen i både dataanalysens och utvecklingens arbetsflöden automatiserades, och sättet som arbetet rörde sig från utveckling till leverans på standardiserades, så att det blev likadant varje gång i stället för att uppfinnas på nytt för varje projekt. En stor del av det var att föra in analyssidan i samma disciplinerade pipeline som utvecklingsarbetet — den hade behandlats som en separat, mer manuell värld.</p>\n<p><strong>Resultat.</strong> De effektiviserade processerna sparade ungefär 4 000 timmar och snabbade upp både dataanalysen och mjukvaruutvecklingen, och lika användbart var att det som levererades blev mer konsekvent — färre överraskningar från arbete som hade gjorts på ett lite annorlunda sätt varje gång.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Dataanalys", "Datapipelines (ETL/ELT)", "DevOps", "Python", "Dataanalys och BI‑dashboards", "DevOps och CI/CD‑automatisering", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/", "url": "https://engineer.company/sv/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/", "title": "Utvecklade ett rapportsystem för dataanalys som ökade de kvartalsvisa mjukvaruintäkterna med 400 % genom Python‑baserade PDF‑rapporter.", "summary": "Byggde ett rapporteringssystem för dataanalys i Python som ökade de kvartalsvisa mjukvaruintäkterna med 400 % genom tydliga PDF-rapporter i rätt tid.", "content_html": "<p><strong>Situation.</strong> Intressenterna fick inte tillgång till analyser i tid och i en läsbar form. Datan fanns, men att göra om den till något man faktiskt kunde fatta beslut utifrån gick långsamt och manuellt, så insynen i hur det gick för verksamheten släpade efter, och de kommersiella besluten släpade efter med den.</p>\n<p><strong>Uppgift.</strong> Att bygga ett rapportsystem för dataanalys — ett som gjorde om rådata till tydliga, regelbundna insikter utan att någon behövde sätta ihop dem för hand varje gång — var jobbet.</p>\n<p><strong>Handling.</strong> Ett rapportsystem genererade PDF‑rapporter i Python och automatiserade hela kedjan: att hämta datan, köra analysen och presentera den i ett rent, enhetligt format som intressenterna faktiskt kunde läsa. Poängen var regelbundenhet och tydlighet — samma professionella rapport som kom förutsägbart, så att siffrorna blev något folk tittade på som en självklarhet snarare än något de var tvungna att gå och gräva fram.</p>\n<p><strong>Resultat.</strong> Det var den rapporteringen som drev upp de kvartalsvisa mjukvaruintäkterna med 400 %. Att göra analysen bättre och snabbare var ingen trevlig bisak i backoffice — lägg tydliga siffror i rätt tid framför dem som fattar de kommersiella besluten så blir besluten bättre, och här syntes det direkt i intäkterna.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Backendutveckling", "Data engineering", "Dataanalys", "Intressenter och rapportering", "Produkt och krav", "Python", "Backend- och API‑utveckling", "Dataanalys och BI‑dashboards", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/automated-data-processing-tasks-using-shell-scripting-pl-45/", "url": "https://engineer.company/sv/portfolio/automated-data-processing-tasks-using-shell-scripting-pl-45/", "title": "Automatiserade uppgifter inom databearbetning med Shell‑skript, PL/pgSQL, Python och Transact‑SQL och ökade produktiviteten och effektiviteten.", "summary": "Automatiserade databehandling med Shell, PL/pgSQL, Python och Transact-SQL, höjde produktiviteten och fick bort små återkommande fel.", "content_html": "<p><strong>Situation.</strong> Det fanns en jämn arbetsbörda av återkommande databehandling som sköttes för hand. Manuellt dataarbete har två problem på en gång: det äter tid, och det är inkonsekvent — gör man samma uppgift för hand tillräckligt många gånger blir den utförd lite olika, och en del av de skillnaderna är fel.</p>\n<p><strong>Uppgift.</strong> Målet var att automatisera de här uppgifterna, både för att få tillbaka tiden och för att göra dem tillförlitliga.</p>\n<p><strong>Handling.</strong> Databehandlingen automatiserades i de databaser och system den berörde, med det som passade för jobbet — Shell‑skript som lim, PL/pgSQL och Transact‑SQL nere i databaserna, Python där det behövdes mer än SQL kunde ge. Manuella steg ersattes med jobb som kördes på samma sätt varje gång, vilket är hela poängen: ett skript blir inte uttråkat, hoppar inte över ett steg och gör det inte annorlunda en fredagseftermiddag.</p>\n<p><strong>Resultat.</strong> Både produktiviteten och effektiviteten ökade, det manuella arbetet lyftes av folks bord, och databehandlingen blev konsekvent och pålitlig i stället för en källa till små, återkommande fel.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "PostgreSQL", "Python", "SQL", "Databasadministration (DBA)", "DevOps och CI/CD‑automatisering", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/", "url": "https://engineer.company/sv/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/", "title": "Effektiviserade projektarbetet och sparade 150 timmar per månad i 30 projekt genom att optimera arbetsflöden och resurshantering.", "summary": "Sparade ~150 timmar per månad i 30 projekt genom optimerade arbetsflöden och resurshantering — en återkommande vinst för hela portföljen.", "content_html": "<p><strong>Situation.</strong> I en portfölj med runt trettio projekt gick tid förlorad varenda månad på arbetsflöden som aldrig hade optimerats och en resurshantering som var ojämn — en del personer underutnyttjade, en del överbelastade, arbetet planerat olika från ett projekt till nästa.</p>\n<p><strong>Uppgift.</strong> Målet var att göra portföljen effektivare och få tillbaka den tid som försvann månad efter månad.</p>\n<p><strong>Handling.</strong> I en genomgång av de trettio projekten bearbetades arbetsflödena och resurshanteringen tillsammans — flaskhalsarna togs bort, arbetslasten balanserades så att det inte alltid var samma fåtal personer som var begränsningen, och sättet att planera och driva arbetet standardiserades så att varje projekt inte var ett eget specialfall. Trettio projekt som vart och ett tappar lite tid blir mycket tillsammans; lösningen handlade mest om att göra de goda rutinerna konsekventa snarare än att hitta på något smart.</p>\n<p><strong>Resultat.</strong> Förändringarna sparade runt 150 timmar i månaden i portföljen. Det är en återkommande månatlig besparing, inte en engångseffekt — trettio projekt som drivs slimmare, månad efter månad.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Automatisering och CI/CD", "Prestandaoptimering", "Projektledning", "Teamledning", "Projektledning (agil)", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/managed-a-team-delivering-it-support-data-recovery-47/", "url": "https://engineer.company/sv/portfolio/managed-a-team-delivering-it-support-data-recovery-47/", "title": "Ledde ett team som levererade IT‑support, dataräddning och hårdvarureparationer till över 1 000 kunder och säkerställde service av hög kvalitet.", "summary": "Ledde ett team som levererade IT-support, dataräddning och hårdvarureparationer till 1 000+ kunder och byggde ett rykte för tillförlitlig service.", "content_html": "<p><strong>Situation.</strong> Företaget var dit en stor kundbas vände sig när deras IT gick sönder — support, dataräddning, hårdvarureparationer, hela spannet. Att möta den sortens jämna, oglamorösa efterfrågan handlar inte om hjältedåd; det handlar om att ha ett team som fungerar bra dag efter dag, eftersom arbetet aldrig riktigt slutar komma in.</p>\n<p><strong>Uppgift.</strong> Jobbet var att leda teamet som levererade allt det, och att hålla kvaliteten jämn oavsett om det var en lugn vecka eller om allt kom på en gång.</p>\n<p><strong>Handling.</strong> Teamet skötte IT‑support, dataräddning och hårdvarureparationer åt över tusen kunder. Mycket av det var organiseringen under ytan — att se till att arbete plockades upp och inte tappades bort, att sätta en standard för vad ”lagat” faktiskt betydde så att folk inte fick tillbaka halvfärdiga reparationer, och att hålla teamet effektivt när ärendekön var lång. Dataräddning i synnerhet är arbete man inte kan ta lätt på; det är oftast någons foton eller företag som ligger på den disken, och de har redan en dålig dag när de väl hör av sig.</p>\n<p><strong>Resultat.</strong> Teamet betjänade över tusen kunder och byggde upp ett verkligt rykte för pålitlig service. I den sortens verksamhet är ryktet allt — folk kommer tillbaka, och de berättar för andra, just för att det faktiskt blev löst förra gången något gick sönder.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Intressenter och rapportering", "Mentorskap och coachning", "Systemadministration", "Teamledning", "Hårdvarureparation och dataräddning", "IT‑support och helpdesk", "Teambygge och mentorskap" ] }, { "id": "https://engineer.company/sv/portfolio/configured-and-deployed-1-000-wi-fi-routers-48/", "url": "https://engineer.company/sv/portfolio/configured-and-deployed-1-000-wi-fi-routers-48/", "title": "Konfigurerade och driftsatte 1 000 Wi‑Fi‑routrar och förbättrade nätverkets tillgänglighet och prestanda för kunderna.", "summary": "Konfigurerade och installerade 1 000 Wi-Fi-routrar enligt en standarduppsättning och gav kunderna pålitlig trådlös åtkomst och prestanda.", "content_html": "<p><strong>Situation.</strong> Kunderna behövde trådlöst nätverk som bara fungerade, och det handlade i grunden om att konfigurera och rulla ut ett stort antal Wi‑Fi‑routrar — och att göra det på samma noggranna sätt varje gång, eftersom en slarvigt uppsatt router antingen är osäker eller långsam, och oftast får man veta vilket först senare.</p>\n<p><strong>Uppgift.</strong> Jobbet var att konfigurera och driftsätta de routrarna för att ge kunderna bättre nätverksåtkomst och prestanda.</p>\n<p><strong>Handling.</strong> Tusen Wi‑Fi‑routrar konfigurerades och driftsattes. Knepet vid den volymen är att standardisera uppsättningen — en enhetlig, säker och förnuftig konfiguration — i stället för att finjustera var och en från grunden på plats, eftersom tusen handgjorda routrar är tusen olika saker att ge support för senare. Så de sattes upp för säkerhet och prestanda på samma sätt varje gång, och rullades ut tillförlitligt hos kunderna.</p>\n<p><strong>Resultat.</strong> De tusen routrarna gav kunderna pålitligt trådlöst nätverk — bättre åtkomst, bättre prestanda — och gjorde det konsekvent, eftersom uppsättningen var standardiserad snarare än improviserad. Målet är en router som ingen behöver tänka på igen; de flesta av dem kom dit.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Nätverk och VPN", "Systemadministration", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "IT‑support och helpdesk", "Nätverk och VPN‑uppsättning" ] }, { "id": "https://engineer.company/sv/portfolio/managed-4-000-computer-repairs-ensuring-rapid-and-49/", "url": "https://engineer.company/sv/portfolio/managed-4-000-computer-repairs-ensuring-rapid-and-49/", "title": "Hanterade 4 000 datorreparationer och säkerställde snabb och effektiv lösning av hård- och mjukvaruproblem.", "summary": "Genomförde 4 000 datorreparationer med snabb, effektiv lösning av hård- och mjukvaruproblem och upprätthöll ett rykte för pålitlig service.", "content_html": "<p><strong>Situation.</strong> Det kom in en konstant ström av datorreparationer — fyratusen över tid — och var och en var någon som väntade på att få tillbaka sin dator och komma vidare med sin dag. Hårdvarufel, mjukvarufel, hela blandningen, och kunderna bedömde verkstaden efter hur snabbt och hur ordentligt felen åtgärdades.</p>\n<p><strong>Uppgift.</strong> Jobbet var att hantera de reparationerna — att se till att problem löstes snabbt och, lika viktigt, korrekt.</p>\n<p><strong>Handling.</strong> Fyratusen datorreparationer gick igenom — diagnos av hårdvaru- och mjukvarufel, ett arbetsflöde organiserat så att datorerna rörde sig framåt i stället för att hopa sig, och en kvalitet som hölls så att en reparation faktiskt blev rätt gjord i stället för att skickas ut igen för att gå sönder nästa vecka. En reparation som kommer tillbaka är värre än en långsam; den kostar kunden en extra resa och kostar verkstaden förtroendet. Så genomströmningen spelade roll, men aldrig på bekostnad av att laga det rätt första gången.</p>\n<p><strong>Resultat.</strong> Alla fyratusen löstes, och snabbt. Folk fick tillbaka fungerande datorer och kunde gå vidare, och företagets rykte för pålitlig service höll — vilket i en reparationsverkstad är hela affären.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Systemadministration", "Hårdvarureparation och dataräddning", "IT‑support och helpdesk" ] }, { "id": "https://engineer.company/sv/portfolio/administered-100-bare-bone-servers-physical-networks-and-50/", "url": "https://engineer.company/sv/portfolio/administered-100-bare-bone-servers-physical-networks-and-50/", "title": "Administrerade 100 Bare Bone‑servrar, fysiska nätverk och system för IP‑telefoni och säkerställde en robust infrastruktur för företagets tillväxt.", "summary": "Administrerade 100 barebone-servrar, fysiska nätverk och IP-telefoni och gav företaget den robusta grund det växte på.", "content_html": "<p><strong>Situation.</strong> Under allt som företaget gjorde låg det fysiska — servrar, de faktiska näten, IP‑telefonin — och allt det måste helt enkelt fungera. Det lagret är osynligt när det fungerar och extremt synligt i samma ögonblick som det inte gör det, och företagets tillväxt vilade på att det förblev pålitligt.</p>\n<p><strong>Uppgift.</strong> Jobbet var att administrera den infrastrukturen och hålla den stabil i takt med att företaget växte.</p>\n<p><strong>Handling.</strong> Hundra barebone‑servrar, tillsammans med de fysiska näten och IP‑telefonisystemen, togs om hand — uppsättningen, underhållet, felsökningen när något gick fel. Barebone‑servrar innebär att man hanterar hårdvaran direkt, så det finns en praktisk, fysisk sida av det: kablaget, lådorna, telefonsystemet som alla märker i samma sekund som ett samtal bryts. Jobbet var att hålla allt det tråkigt, i ordets goda bemärkelse.</p>\n<p><strong>Resultat.</strong> Servrarna, näten och telefonin fungerade tillförlitligt, och den pålitliga fysiska grunden är det som lät företaget fortsätta växa utan att marken gungade under det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Nätverk och VPN‑uppsättning" ] }, { "id": "https://engineer.company/sv/portfolio/developed-100-web-applications-using-html-html5-css-51/", "url": "https://engineer.company/sv/portfolio/developed-100-web-applications-using-html-html5-css-51/", "title": "Utvecklade 100 webbapplikationer med HTML/HTML5, CSS/SCSS och ramverken Django, WordPress och Joomla och säkerställde en mångsidig närvaro på nätet.", "summary": "Utvecklade 100 webbapplikationer med HTML5, CSS/SCSS, Django, WordPress och Joomla och anpassade varje lösning efter kundens verkliga behov.", "content_html": "<p><strong>Situation.</strong> Kunderna ville ha webbapplikationer, och de ville ha väldigt olika saker — olika skalor, olika budgetar, olika grader av ”se bara till att jag finns på nätet” kontra ”bygg något skräddarsytt åt mig”. Att möta det krävde mångsidighet i stället för att tvinga in varje kund i samma teknik.</p>\n<p><strong>Uppgift.</strong> Jobbet var att bygga webbapplikationer som gav varje kund en varierad och effektiv närvaro på nätet.</p>\n<p><strong>Handling.</strong> Hundra webbapplikationer byggdes, och poängen var att matcha verktyget mot jobbet snarare än att ha en favorit. Där en kund behövde något skräddarsytt blev det HTML/HTML5, CSS/SCSS och Django; där kunden behövde något mer standardmässigt som den också kunde sköta själv var WordPress eller Joomla det snabbare och förnuftigare svaret. En del av kunnandet var att veta vilket som var vilket — att få en kund att avstå från ett specialbygge den inte behövde, eller att övertyga den om ett som den faktiskt behövde.</p>\n<p><strong>Resultat.</strong> De hundra applikationerna gav kunderna en varierad och kapabel närvaro på nätet, var och en levererad på den teknik som faktiskt passade den. Att anpassa angreppssättet efter kunden snarare än tvärtom är det som gjorde dem effektiva och inte bara levererade.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Frontendutveckling", "Fullstackutveckling", "Python", "Webbutveckling", "Fullstackutveckling av produkter", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/designed-30-websites-delivering-unique-and-flexible-solutions-52/", "url": "https://engineer.company/sv/portfolio/designed-30-websites-delivering-unique-and-flexible-solutions-52/", "title": "Utformade 30 webbplatser och levererade unika och flexibla lösningar genom att omvandla Photoshop‑designer till HTML.", "summary": "Utformade 30 webbplatser genom att konvertera Photoshop-design till HTML och skapade särpräglade, polerade och underhållbara webbnärvaror.", "content_html": "<p><strong>Situation.</strong> Kunderna kom med ett utseende de ville ha — ofta en färdig visuell design — och behövde en webbplats som byggdes troget efter den. Glappet mellan en designfil och en fungerande webbplats är där mycket kvalitet vinns eller förloras: det är lätt att leverera något som är ungefär rätt och subtilt fel.</p>\n<p><strong>Uppgift.</strong> Jobbet var att designa webbplatserna och omsätta designerna i exakta, flexibla implementationer.</p>\n<p><strong>Handling.</strong> Trettio webbplatser designades och Photoshop‑designerna omsattes för hand till HTML. Trogenhet var måttstocken — avstånden, typografin, detaljerna som designern faktiskt avsåg, inte en approximation av dem — men flexibilitet var det också, eftersom en webbplats som matchar mockupen pixel för pixel och sedan faller isär så fort innehållet ändras inte riktigt har byggts väl. Så målet var markup som förblev trogen designen och förblev underhållbar efteråt.</p>\n<p><strong>Resultat.</strong> De trettio webbplatserna matchade sina designer och förblev hanterbara — särpräglade, genomarbetade webbnärvaror som inte gick sönder första gången någon redigerade dem. Trogen designen och ändå underhållbar är den balans som spelade roll, och det är där de här landade.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Frontendutveckling", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/administered-40-websites-on-ubuntu-linux-hosting-servers-53/", "url": "https://engineer.company/sv/portfolio/administered-40-websites-on-ubuntu-linux-hosting-servers-53/", "title": "Administrerade 40 webbplatser på värdservrar med Ubuntu Linux, Apache och Nginx och säkerställde hög tillgänglighet och prestanda.", "summary": "Administrerade 40 webbplatser på Ubuntu Linux med Apache och Nginx, med hög tillgänglighet och prestanda — webbhotell som kunderna aldrig behövde tänka på.", "content_html": "<p><strong>Situation.</strong> Det fanns en portfölj med webbplatser i drift som behövde vara uppe och vara snabba — och hosting är ännu ett av de jobb som är osynliga tills en webbplats går ner, och då är det det enda någon bryr sig om.</p>\n<p><strong>Uppgift.</strong> Jobbet var att administrera de webbplatserna och hålla dem snabba och med hög tillgänglighet.</p>\n<p><strong>Handling.</strong> Fyrtio webbplatser kördes på hostingservrar med Ubuntu Linux, på en blandning av Apache och Nginx — konfigurationen, prestandaoptimeringen, det löpande underhållet för att hålla dem tillförlitliga under verklig trafik. Verklig trafik är det avgörande: en webbplats som fungerar när ingen använder den och faller ihop när någon gör det har inte administrerats, den har bara lämnats i fred. Så arbetet gick ut på att hålla dem friska under faktisk belastning.</p>\n<p><strong>Resultat.</strong> Alla fyrtio drevs med hög tillgänglighet och god prestanda, vilket gav kunderna hosting de inte behövde tänka på. Stabil och pålitlig under verklig användning är hela poängen med hosting, och det är vad de här levererade.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Prestandaoptimering", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Webbutveckling", "Tillförlitlig drift och övervakning", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/engineered-600-pl-pgsql-based-etl-elt-pipelines-54/", "url": "https://engineer.company/sv/portfolio/engineered-600-pl-pgsql-based-etl-elt-pipelines-54/", "title": "Byggde 600 PL/pgSQL‑baserade ETL/ELT‑pipelines för att effektivisera komplexa arbetsflöden för databearbetning i flera utvecklings- och produktionsmiljöer för PostgreSQL.", "summary": "Byggde 600 ETL/ELT-pipelines i PL/pgSQL för PostgreSQL i dev och prod, kortade den genomsnittliga körtiden 35 % och ökade genomströmningen med över 50 %.", "content_html": "<p><strong>Situation.</strong> Organisationen hanterade stora volymer transaktions- och analysdata i flera PostgreSQL‑miljöer för utveckling och produktion. De datapipelines som fanns var fragmenterade, saknade konsekvens och orsakade prestandaflaskhalsar, vilket ledde till förseningar i affärskritisk rapportering och beslutsfattande.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att utforma och implementera robusta, skalbara och effektiva ETL/ELT‑pipelines med PL/pgSQL för att effektivisera komplexa arbetsflöden för inläsning, transformering och laddning av data. Ett centralt mål var att förbättra prestandan för databasfrågor och säkerställa dataintegriteten i alla miljöer.</p>\n<p><strong>Handling.</strong> Över 600 PL/pgSQL‑baserade ETL/ELT‑pipelines byggdes för att automatisera extrahering, transformering och laddning av data från olika källor. De viktigaste tekniska implementationerna omfattade:</p>\n<ul>\n<li>Utnyttjade partitionerade tabeller och materialiserade vyer för att optimera läsprestandan för stora datamängder.</li>\n<li>Tillämpade villkor för primärnycklar och främmande nycklar för att upprätthålla referensintegriteten under transformeringarna.</li>\n<li>Utformade unika och sammansatta index för att snabba upp JOIN‑operationer och komplexa filtreringsvillkor.</li>\n<li>Byggde in undantagshantering och transaktionsstyrning för att säkerställa feltolerans och rollback vid fel.</li>\n<li>Möjliggjorde inkrementella laddningar med mekanismer för change data capture (CDC) och tidsstämpelbaserade deltan, vilket minskade bearbetningstiden med över 60 %.</li>\n<li>Utvecklade automatiserade procedurer för loggning och granskning för att spåra pipelinekörningar, övervaka avvikelser och underlätta felsökning.</li>\n</ul>\n<p><strong>Resultat.</strong> Det nya ETL/ELT‑ramverket förbättrade avsevärt konsekvensen, tillförlitligheten och prestandan i databehandlingens arbetsflöden:</p>\n<ul>\n<li>Uppnådde 35 % kortare genomsnittlig körtid per pipeline.</li>\n<li>Ökade genomströmningen i datapipelines med över 50 %, vilket möjliggjorde datatillgänglighet i nära realtid för rapportering.</li>\n<li>Minskade datakvalitetsproblemen genom att eliminera över 90 % av transformeringsfelen tack vare bättre tillämpning av villkor och valideringslogik.</li>\n<li>Förbättrade underhållbarheten och skalbarheten, med stöd för framtida ändringar i datamodellen med minimalt omarbete.</li>\n</ul>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "Datastyrning", "PostgreSQL", "Prestandaoptimering", "SQL", "Databasadministration (DBA)", "Datastyrning och datakvalitet", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/architected-developed-implemented-supported-infrastructure-data-processing-and-55/", "url": "https://engineer.company/sv/portfolio/architected-developed-implemented-supported-infrastructure-data-processing-and-55/", "title": "Utformade, utvecklade och implementerade infrastrukturen, databearbetningen och kartapplikationen och gav support för dem i 2 år i sträck utan en enda ledig helg, helgdag eller semester, 10–14 timmar om dagen.", "summary": "Utformade, byggde och drev infrastrukturen, databehandlingen och kartappen i två år utan avbrott — produktens pålitliga ryggrad.", "content_html": "<p><strong>Situation.</strong> Ett startupföretag inom grön energi i ett tidigt skede var beroende av en enda plattform för att spåra, övervaka och optimera anläggningar för förnybar energi, men hade varken ett dedikerat infrastrukturteam eller en etablerad ingenjörsorganisation som kunde bygga och driva den. Hela den tekniska grunden — molninfrastrukturen, pipelines för databehandling och den kundvända GIS‑kartapplikationen — måste skapas och hållas igång kontinuerligt, på en marknad där varje driftstopp eller lucka i datan direkt urholkade kundernas förtroende och intäkterna.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att på egen hand stå för arkitekturen, bygget och driften av hela systemet från början till slut, inom såväl plattforms- och datateknik som DevOps och site reliability. Utöver att skriva mjukvaran innebar det att ansvara för produktionen: att provisionera och härda infrastrukturen, utforma det databehandlingslager som försåg kartan med data och garantera att applikationen förblev tillgänglig dygnet runt för en växande kundbas — allt inom de begränsningar och det obevekliga tempo som präglar en snabbrörlig startup.</p>\n<p><strong>Handling.</strong> Under två år utformades och implementerades infrastrukturen, datapipelines och kartapplikationen, och de fick löpande support utan avbrott — inga helger, helgdagar eller semestrar, ofta tio till fjorton timmar om dagen. En pragmatisk, modulär arkitektur valdes för att hålla en enmansverksamhet underhållbar, med automatiserad provisionering, övervakning och larm så att problem kunde upptäckas och lösas snabbt. Databehandlingen finjusterades kontinuerligt för tillförlitlighet och prestanda, releaser levererades stegvis, och varje lager — från servrarna till kartan som användarna mötte — underhölls och förbättrades egenhändigt utifrån kundernas faktiska användning.</p>\n<p><strong>Resultat.</strong> Plattformen förblev ständigt tillgänglig och utvecklades från en bräcklig tidig prototyp till produktens pålitliga ryggrad, och bar företaget genom dess kritiska tillväxtfas tack vare en enda ingenjörs ansvarstagande. Det praktiska förvaltarskapet höll infrastrukturen, datan och kartapplikationen tillräckligt tillförlitliga för att stödja merförsäljning, datalicensiering och nykundsförvärv, och visade en sällsynt grad av engagemang, bredd och helhetsansvar genom hela stacken.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Datapipelines (ETL/ELT)", "Fullstackutveckling", "GIS / geodata", "Infrastruktur", "Plattformsarkitektur", "Tillförlitlighet och säkerhetskopiering", "Fullstackutveckling av produkter", "GIS- och geodatalösningar", "Plattforms- och lösningsarkitektur", "Tillförlitlig drift och övervakning", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/optimized-budget-costs-10-times-with-zero-loss-56/", "url": "https://engineer.company/sv/portfolio/optimized-budget-costs-10-times-with-zero-loss-56/", "title": "Gjorde budgetkostnaderna 10 gånger lägre utan någon förlust i produktivitet för en internationell kund genom att tänka om hela infrastrukturen, ta bort onödiga tjänster och flytta från AWS‑molnet.", "summary": "Sänkte infrastrukturbudgeten 10 gånger utan produktivitetsförlust för en internationell kund genom att tänka om stacken och lämna AWS.", "content_html": "<p><strong>Situation.</strong> En internationell kund bar på kraftigt uppblåsta infrastrukturkostnader. Kundens AWS‑miljö hade överdimensionerats och samlat på sig tjänster som inte längre användes, så molnfakturan hade glidit helt ur proportion till vad verksamheten faktiskt behövde. Det är en vanlig historia — ingen bestämmer sig för att spendera för mycket, det ackumuleras bara när ingen håller koll på mätaren.</p>\n<p><strong>Uppgift.</strong> Uppdraget var att sänka kostnaderna kraftigt utan att förlora någon produktivitet, vilket innebar att tänka om infrastrukturen ordentligt i stället för att putsa i kanterna — att putsa i kanterna biter sällan på en faktura som är strukturellt för stor.</p>\n<p><strong>Handling.</strong> Så arbetet omfattade hela kedjan. Först kom en granskning av vad som faktiskt användes — det är där de dubblerade och onödiga tjänsterna visar sig — och de togs bort. Sedan dimensionerades det som var kvar efter verklig efterfrågan i stället för efter de gissningar på värsta tänkbara fall som den ursprungliga miljön hade byggts på. Och det stora draget var att flytta arbetslasterna bort från AWS helt och hållet, till en mer kostnadseffektiv hostinglösning — varsamt och i etapper, så att den löpande verksamheten aldrig märkte migreringen som pågick under den.</p>\n<p><strong>Resultat.</strong> Budgetkostnaderna sjönk ungefär tiofalt, utan någon som helst förlust av produktivitet — samma kapacitet till en bråkdel av vad kunden hade betalat. Det frigjorde en rejäl summa pengar som i det tysta hade läckt ut i en överdimensionerad molnfaktura månad efter månad, pengar som för verksamheten gick raka vägen tillbaka till sista raden.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "DevOps", "Infrastruktur", "Lösningsarkitektur", "Migrering och modernisering", "Molntjänster", "Plattformsarkitektur", "DevOps och CI/CD‑automatisering", "Molninfrastruktur och migrering", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/architected-a-layered-maritime-platform-separating-a-next-57/", "url": "https://engineer.company/sv/portfolio/architected-a-layered-maritime-platform-separating-a-next-57/", "title": "Utformade arkitekturen för en skiktad sjöfartsplattform som håller isär en PWA‑frontend i Next.js, ett API i Go (Huma/Fiber) och ett funktionsskikt i PostgreSQL, med all affärslogik i databasen.", "summary": "Utformade en skiktad sjöfartsplattform — Next.js-PWA, ett Go-API (Huma/Fiber) och ett funktionslager i PostgreSQL som rymmer all affärslogik.", "content_html": "<p><strong>Situation.</strong> Produkten skulle bli en maritim plattform — professionellt nätverkande, företagsrecensioner, sponsrad utbildning — och det var en ung produkt, vilket är ett artigt sätt att säga att kraven skulle komma att ändras en hel del. Det som skulle undvikas var en arkitektur där en ändrad affärsregel innebar att frontenden, API:et och databasen måste röras samtidigt. I en liten kodbas som växer snabbt är det den sortens koppling som gör en ändring på två rader till ett eftermiddagsjobb.</p>\n<p><strong>Uppgift.</strong> I rollen som arkitekt gällde det att redan från början avgöra var varje sorts logik skulle ligga, med gränser tydliga nog att hålla under press i stället för att suddas ut första gången någon hade bråttom.</p>\n<p><strong>Handling.</strong> Det landade i tre lager, med ett jobb var. Next.js‑frontenden sköter presentation och interaktivitet och ingenting annat. Go‑API:et — Huma ovanpå Fiber — är medvetet tunt: det routar, validerar förfrågan, tillämpar säkerhetsfiltreringen och orkestrerar, men innehåller ingen affärslogik alls. All affärslogik ligger i PostgreSQL‑funktioner, som sätter ihop hela resultatet och lämnar tillbaka det för API:et att vidarebefordra. Så när en regel ändras, ändras den i ett lager, i SQL, och de andra två behöver inte veta om det. Gränserna skrevs ned och upprätthölls sedan i kodgranskningen, eftersom en konvention som ingen bevakar slutar vara en konvention.</p>\n<p><strong>Resultat.</strong> Det hela förblev lätt att hålla i huvudet. Affärslogiken sitter på ett ställe som faktiskt går att granska, API:et är en tråkig adapter i ordets goda bemärkelse, och frontenden bryr sig inte när schemat förändras under den. Den uppdelningen är det som lät produkten fortsätta bygga på med nya funktioner utan att arkitekturen i det tysta ruttnade.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Databaser", "Frontendutveckling", "Fullstackutveckling", "Lösningsarkitektur", "Plattformsarkitektur", "PostgreSQL", "Tekniskt ledarskap", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Fullstackutveckling av produkter", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/designed-a-json-passthrough-architecture-where-postgresql-functions-58/", "url": "https://engineer.company/sv/portfolio/designed-a-json-passthrough-architecture-where-postgresql-functions-58/", "title": "Utformade en arkitektur för direkt vidarebefordran av JSON, där PostgreSQL‑funktioner returnerar komplett JSON som Go‑API:t skickar vidare ordagrant, vilket tog bort mellanliggande deserialisering och frikopplade frontenden från schemaändringar.", "summary": "Utformade direkt vidarebefordran av JSON: PostgreSQL-funktioner returnerar komplett JSON som Go-API:t skickar oförändrad, så frontenden frikopplas från schemat.", "content_html": "<p><strong>Situation.</strong> Vanligtvis tar sig data från en databas till en webbläsare genom en stafett av transformationer. Databasen lämnar rader till API:et, API:et deserialiserar dem till structar, formar om dem, serialiserar dem tillbaka till JSON, och först därefter skickas de ut. Varje sådant steg är kod man skriver, kod man testar och ytterligare ett ställe där API:ets bild av datan och databasens bild av den kan glida isär.</p>\n<p><strong>Uppgift.</strong> Idén var att hoppa över stafetten. Om databasen kunde returnera det färdiga svaret kunde API:et bara skicka det vidare, och frontenden kunde vara beroende av databasens form direkt i stället för av en handunderhållen kopia av den som låg i Go‑koden.</p>\n<p><strong>Handling.</strong> Så det byggdes för att skicka svaret rakt igenom. PostgreSQL‑funktionerna sätter ihop hela svaret som JSON — formningen hör hemma i SQL och görs där datan redan finns. Go‑handlern tar emot det som json.RawMessage och vidarebefordrar det orört; den bryter aldrig ned det och kodar aldrig om det. En liten QueryJSON‑hjälpfunktion gjorde det mönstret till minsta motståndets väg i stället för något man behövde komma ihåg att göra. Det som föll bort var allt det mellanliggande maskineri som ett konventionellt lagerindelat API samlar på sig — DTO:erna, mappningsfunktionerna, svars‑structarna.</p>\n<p><strong>Resultat.</strong> Handlerkoden blev dramatiskt kortare, och framför allt slutade frontenden vara kopplad till Go. Ändrar man vad en funktion returnerar flödar den nya formen rakt igenom till klienten, utan att någon ändrar en enda rad handlerkod. Färre rörliga delar, och en hel kategori av glidning mellan lagren existerar helt enkelt inte här.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Databaser", "Plattformsarkitektur", "PostgreSQL", "Prestandaoptimering", "SQL", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/designed-an-organization-context-switching-system-with-client-59/", "url": "https://engineer.company/sv/portfolio/designed-an-organization-context-switching-system-with-client-59/", "title": "Utformade ett system för att växla organisationskontext med localStorage hos klienten och en speglad cookie på serversidan, så att användare kan agera som de organisationer de förvaltar medan auktorisering enligt minsta behörighet upprätthålls.", "summary": "Byggde organisationsbyte med localStorage och spegling i servercookie, så att användare agerar som hanterade organisationer med minsta möjliga behörighet.", "content_html": "<p><strong>Situation.</strong> På plattformen kan en yrkesverksam inom sjöfarten administrera organisationer — företag, akademier — som saknar egna inloggningar. Personen är kontot; organisationen är något personen agerar på uppdrag av. En användare behöver därför kunna röra sig genom hela appen som vilken som helst av de organisationer kontot administrerar och växla fritt mellan dem, och den bekvämligheten får inte bli ett hål i behörighetskontrollen.</p>\n<p><strong>Uppgift.</strong> Kontextbytet behövde gå snabbt och diskret för användaren, samtidigt som den aktiva kontexten aldrig på egen hand fick kunna ge någon åtkomst som personen inte hade rätt till.</p>\n<p><strong>Handling.</strong> En OrganizationContext sköter det, med lagring på båda sidor. På klienten är localStorage sanningskällan för vilken organisation du just nu agerar som, så bytet sker direkt — utan att gå via servern. En cookie på serversidan speglar valet så att serverrenderade sidor får samma kontext under SSR; på servern finns en getServerViewMode som läser den. Det avgörande är att inget av detta betraktas som tillförlitligt när åtkomst ska beslutas. Behörigheten kontrolleras på nytt på servern vid varje anrop. Kontexten i frontenden finns där för upplevelsen — att visa dig rätt saker — och servern är ensam om att avgöra vad du får göra.</p>\n<p><strong>Resultat.</strong> En användare kan utan friktion agera som vilken som helst av de organisationer kontot administrerar, och gränssnittet hålls synkroniserat på både klient och server. Men eftersom behörigheterna verifieras på serversidan varje gång sker ingenting av den bekvämligheten på säkerhetens bekostnad. Den som manipulerar innehållet i localStorage ändrar vad det egna gränssnittet visar och ingenting mer — servern säger fortfarande nej.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Frontendutveckling", "Fullstackutveckling", "Plattformsarkitektur", "Säkerhet", "Backend- och API‑utveckling", "Plattforms- och lösningsarkitektur", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/migrated-the-http-api-from-fiber-to-huma-60/", "url": "https://engineer.company/sv/portfolio/migrated-the-http-api-from-fiber-to-huma-60/", "title": "Migrerade HTTP‑API:t från Fiber till Huma v2 — 649 sökvägar och 760 operationer — och nådde och behöll 100 % paritet mellan de rutter som servern registrerar och den OpenAPI‑beskrivning den publicerar.", "summary": "Migrerade HTTP-API:t från Fiber till Huma v2 — 649 sökvägar, 760 operationer — med 100 % paritet mellan registrerade rutter och publicerad OpenAPI-beskrivning.", "content_html": "<p><strong>Situation.</strong> API:et började sitt liv på Fiber, med validering av anrop som skrevs för hand, slutpunkt för slutpunkt. Det fungerar när det finns en handfull slutpunkter. Det slutar fungera när ytan växer: den handskrivna valideringen blir en ständig underhållsbörda, och små inkonsekvenser smyger sig in eftersom varje slutpunkts kontroller är ett eget litet specialfall. Och det fanns ingenstans en samlad beskrivning av hur API:et såg ut.</p>\n<p><strong>Uppgift.</strong> Målet var validering som följde av typerna i stället för av handskrivna kontroller, och ett verkligt kontrakt som beskrev API:et — utan att stanna upp för en omskrivning av allt i ett enda svep.</p>\n<p><strong>Handling.</strong> HTTP‑lagret flyttades till Huma v2, som ligger ovanpå Fiber, så den befintliga körmiljön fick vara kvar. Varje slutpunkt får structar för indata och utdata, och Huma genererar valideringen av anropen och modelleringen av svaren utifrån de typerna. En OpenAPI‑beskrivning kommer på köpet, vilket betyder att dokumentationen håller jämna steg med koden i stället för att ruttna i en wiki. Allt nytt skrevs mot Huma och de befintliga rutterna migrerades över, med exakt två slutpunkter kvar på ren Fiber — de för WebSocket, där du verkligen vill åt själva socketen och Humas modell med anrop och svar inte passar. Det har vuxit till 649 sökvägar med 760 operationer, och en kontroll i CI som jämför de rutter som servern faktiskt registrerar med dem som OpenAPI‑beskrivningen utlovar. Överensstämmelsen är 100 %, och den ligger kvar där eftersom en rutt som saknar beskrivning stoppar bygget.</p>\n<p><strong>Resultat.</strong> Nya slutpunkter får validering och aktuell dokumentation utan att någon behöver lägga extra arbete på det, och en hel klass av buggar i anropshanteringen — av typen ”oj, vi glömde kontrollera det fältet här” — försvann. Med 760 operationer är beskrivningen det enda praktiska sättet för någon att läsa API:et, så garantin att den är komplett väger tyngre än den gjorde vid femtio. Det typade kontraktet gjorde API:et både säkrare att ändra och lättare att lämna över till någon annan, eftersom typerna talar om vad en slutpunkt förväntar sig.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Dokumentation", "Migrering och modernisering", "Plattformsarkitektur", "Test och kvalitetssäkring", "Backend- och API‑utveckling", "Plattforms- och lösningsarkitektur", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-request-schema-validation-contract-with-automated-61/", "url": "https://engineer.company/sv/portfolio/built-a-request-schema-validation-contract-with-automated-61/", "title": "Genererade API‑kontraktet utåt från databasen — OpenAPI, en typad TypeScript‑klient på 44 076 rader, 61 mock‑hanterare och de gränser som gränssnittet upprätthåller — med en spärr i varje led som fäller vid avvikelse.", "summary": "Genererade API-kontraktet från databasen och utåt — OpenAPI, en typad TypeScript-klient, mock-hanterare och UI-gränser — med en kontroll i varje steg.", "content_html": "<p><strong>Situation.</strong> Frontend och backend rör sig i var sin takt, och deras antaganden om nyttolasten i ett anrop kan glida isär utan att någon märker det. Oftast upptäcker man det genom en 422 i webbläsaren — när avvikelsen redan har släppts, vilket är det dyraste tillfället att få veta det. Att skriva båda sidorna för hand utifrån samma dokument löser inte problemet; det flyttar bara isärglidningen till den som glömde att läsa om dokumentet.</p>\n<p><strong>Uppgift.</strong> De två sidorna behövde genereras från en enda källa i stället för att stämmas av mellan två, och varje steg i genereringen skulle kontrolleras i stället för att tas på tro.</p>\n<p><strong>Handling.</strong> Kedjan börjar i databasen och går utåt. Schemat och dess funktioner definierar datastrukturerna; Go‑typerna definierar API:et; Huma genererar OpenAPI‑beskrivningen utifrån dem; en typad TypeScript‑klient — hela 44 076 rader — genereras från den beskrivningen; 61 mock‑hanterare genereras vid sidan av den så att frontendens egna tester körs mot det verkliga kontraktet i stället för mot en handskriven fixture; och de gränser som gränssnittet upprätthåller i ett formulär kommer från samma ställe i stället för att skrivas in på nytt i en validerare. Varje steg har ett skydd. En kontraktskontroll i CI jämför vad frontenden skickar med vad API:et förväntar sig och stoppar bygget vid avvikelser, med en schemasond under som kontrollerar de verkliga strukturerna i stället för en beskrivning av dem. Den täcker medvetet de ställen där isärglidning gärna gömmer sig: valfria fält i anropets body, där ”saknas” och ”null” blandas ihop, och enum‑värden i query‑parametrar, där de två sidorna i det tysta kan vara oense om vilka värden som är tillåtna.</p>\n<p><strong>Resultat.</strong> Ett fält kan inte ändras på bara den ena sidan — det fallerar vid det första steg som märker det, i ett bygge, minuter efter ändringen. Det tog bort en återkommande och genuint irriterande klass av buggar, den sorten som är osynlig vid kodgranskning och bara dyker upp vid körning. Priset är ett genereringssteg mitt i allt: att generera om är ett tråkigt måste, och kedjan är bara så pålitlig som sin sämst skyddade länk, vilket är skälet till att varje steg fick ett skydd.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Backendutveckling", "DevOps", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering" ] }, { "id": "https://engineer.company/sv/portfolio/built-email-as-a-platform-capability-with-failover-62/", "url": "https://engineer.company/sv/portfolio/built-email-as-a-platform-capability-with-failover-62/", "title": "Byggde e‑post som en förmåga i plattformen — tre leverantörer med failover, webhooks för leveranser, loggning av utskick och leveranser, mallar och kampanjer — bakom en startkontroll som vägrar starta utan en leverantör.", "summary": "Byggde e-post som plattformsförmåga: tre leverantörer med failover, leveranswebhooks, loggning av utskick och leveranser, mallar och kampanjutskick.", "content_html": "<p><strong>Situation.</strong> E‑post väger tungt på plattformen — verifiering, aviseringar, sammandrag, kampanjer, det som en användare faktiskt väntar på. Och en e‑postleverantör är precis den sortens beroende som fallerar i det tysta: konfigurationen ser bra ut, appen startar, och du upptäcker att något är trasigt först när en verklig person aldrig får meddelandet som utlovades. Det är det sämsta sättet att få veta det. En enda leverantör gör det värre, eftersom felet då blir totalt och någon annans att åtgärda.</p>\n<p><strong>Uppgift.</strong> E‑post behövde behandlas som en förmåga som plattformen äger snarare än ett klientbibliotek som den anropar — som klarar ett avbrott hos en leverantör, som kan redogöra för vad som hände med ett visst meddelande och som säger ifrån högljutt vid uppstart i de miljöer där tystnad är farlig.</p>\n<p><strong>Handling.</strong> Tre leverantörer sitter bakom ett gemensamt gränssnitt — SendGrid som primär, med SMTP2GO och Azure Communication Services bakom — och failover mellan dem sker automatiskt i stället för som en konfigurationsändring under press. Leveransen tas inte för given: inkommande webhooks rapporterar vad varje leverantör gjorde med ett meddelande, och båda sidorna registreras, i en utskickslogg och en tabell för leveranshändelser, så att ”fick den här personen sitt verifieringsmejl” blir en databasfråga i stället för en gissning. Mallhanteringen håller meddelandetexterna utanför koden, och ett separat schema för massutskick — 5 tabeller och 24 funktioner — levererar kampanjer till användarsegment, vilket är ett annat problem än transaktionell e‑post och byggdes som ett sådant. Framför alltihop skickar en kontroll vid uppstart ett verkligt meddelande genom hela stacken, bakom en flagga: i utveckling loggar den en varning och fortsätter, eftersom ingen vill att den egna laptopen vägrar starta på grund av en utgången sandlådenyckel, och i staging och produktion är ett fel fatalt och processen avslutas i stället för att ett bygge som inte kan skicka e‑post driftsätts. Själva sändningsvägen går genom en SSRF‑skyddad klient med en timeout på 30 sekunder, och den asynkrona leveransvägen har omförsök och backoff så att en tillfällig störning inte tappar ett meddelande.</p>\n<p><strong>Resultat.</strong> En hel kategori av tysta fel flyttades från ”en användare märker det dagar senare” till ”driftsättningen stoppas”, och en leverantör som har en dålig eftermiddag blev ett nedsatt driftläge i stället för ett avbrott. Priset är tre integrationer att hålla vid liv i stället för en, och leveransloggar som växer och måste rensas — båda accepterade, eftersom e‑post är den kanal som plattformen inte kan gå runt.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Molntjänster", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "Säkerhet och behörighetshantering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/designed-a-postgresql-function-first-data-layer-across-63/", "url": "https://engineer.company/sv/portfolio/designed-a-postgresql-function-first-data-layer-across-63/", "title": "Utformade ett funktionsbaserat dataskikt i PostgreSQL — 1 275 lagrade funktioner i 34 scheman — så att varje läsning och skrivning går via en funktion som databasen kan ge behörighet till, i stället för via en tabell.", "summary": "Utformade ett funktionsbaserat datalager i PostgreSQL — 1 275 lagrade funktioner i 34 scheman — där databasen själv upprätthåller åtkomstgränsen.", "content_html": "<p><strong>Situation.</strong> Affärslogik har en tendens att läcka. Lite hamnar i API:et, lite i någon SQL som en hanterare kör direkt i koden, och snart är samma regel skriven på två eller tre lite olika sätt och det finns ingenstans att peka och säga ”det här är vad systemet gör med sina data”. Det är så buggar och säkerhetshål kommer in.</p>\n<p><strong>Uppgift.</strong> Målet var ett enda hem för alltihop: varje läsning och skrivning skulle gå genom databasen, API:et skulle vara en tunn adapter som inte känner till affärsreglerna, och det hela skulle gå att låsa ordentligt.</p>\n<p><strong>Handling.</strong> Datalagret är byggt med funktionerna först. Schemat är uppdelat per domän — identity, organization, review, message, notification och 29 till, 34 totalt — och varje operation som appen kan utföra är en av 1 275 PostgreSQL‑funktioner som den anropar; det finns ingen direkt tabellåtkomst från Go över huvud taget. Sedan ser databasen till att det efterlevs. Rollen som API:et loggar in som, mariner, har EXECUTE på appens funktioner och USAGE på schemana och inget annat — ingen SELECT, ingen INSERT, inget sätt att röra en tabell direkt — vilket blir ungefär 4 000 explicita behörigheter i stället för en generell. Funktionerna körs som SECURITY DEFINER och ägs av en separat roll utan inloggning, function_owner, med en fast search_path, och superanvändarkontot är reserverat för migreringar och cron, långt från den körande appen.</p>\n<p><strong>Resultat.</strong> Logiken bor på ett ställe som faktiskt går att granska, API:et förblir tunt och tråkigt på det bra sättet, och åtkomstgränsen upprätthålls av själva Postgres i stället för av att alla kommer ihåg reglerna. Om API:et på något sätt komprometterades skulle det ändå inte kunna göra något som funktionerna inte tillåter. Med 1 275 funktioner kostar disciplinen på riktigt — ett nytt fält är en migrering och en funktionsändring, inte en rad i en SQL‑fråga — och den friktionen är priset för att gränsen håller.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "Datastyrning", "Plattformsarkitektur", "PostgreSQL", "SQL", "Säkerhet", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Datastyrning och datakvalitet", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/adopted-uuid-v7-time-ordered-identifiers-postgresql-18-64/", "url": "https://engineer.company/sv/portfolio/adopted-uuid-v7-time-ordered-identifiers-postgresql-18-64/", "title": "Införde tidsordnade UUID v7‑identifierare (PostgreSQL 18) som entitetsnycklar för att minska fragmenteringen i B‑trädindex och snabba upp databasfrågor.", "summary": "Införde tidsordnade UUID v7-nycklar (PostgreSQL 18) för mindre fragmentering i B-trädindex och snabbare infogningar och intervallfrågor i alla tabeller.", "content_html": "<p><strong>Situation.</strong> Varje entitet behöver ett unikt id, och reflexvalet är ett slumpmässigt UUID. Problemet är att slumpmässiga id:n hamnar huller om buller i ett B‑trädindex. Insättningarna sprids ut, indexet fragmenteras, och när tabellerna växer får både skrivningar och intervallskanningar betala för det. På en plattform som är tänkt att fortsätta växa är det ett långsamt läckage som man helst inte bygger in.</p>\n<p><strong>Uppgift.</strong> Att behålla den globala unikheten hos ett UUID men bli av med fragmenteringen som slumpmässigheten för med sig.</p>\n<p><strong>Handling.</strong> Standarden blev UUID v7, som är tidsordnat — de inledande bitarna är en tidsstämpel, så nya rader sorteras in i indexet i stället för att strös ut över det. PostgreSQL 18 har detta inbyggt som uuidv7(), som lindades in i en liten uuid_generate_v7()-funktion så att samma anrop fungerar rent på Flexible Server i Azure, och som sedan gjordes till standardvärde för entiteternas primärnycklar i hela schemat. Det är inget exotiskt med det; det är den sortens beslut som är billigt om man fattar det tidigt och besvärligt att bygga in i efterhand.</p>\n<p><strong>Resultat.</strong> Id:na förblev globalt unika, indexet slutade fragmenteras på det sätt som slumpmässiga UUID:n orsakar, och tidsordnade insättningar och intervallfrågor blev snabbare — jämnt, i varje tabell, utan att någon behöver tänka på det igen. Som en bonus får varje entitet en nyckel som du kan sortera på tid, helt gratis.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Data engineering", "Databaser", "PostgreSQL", "Prestandaoptimering", "Databasdesign och datamodellering", "Optimering av databasprestanda" ] }, { "id": "https://engineer.company/sv/portfolio/implemented-a-catalogue-driven-deep-merge-for-stored-65/", "url": "https://engineer.company/sv/portfolio/implemented-a-catalogue-driven-deep-merge-for-stored-65/", "title": "Implementerade en katalogstyrd djupsammanslagning av lagrade JSON‑inställningar som förhindrar krascher på grund av saknade nycklar när schemat utvecklas.", "summary": "Implementerade en katalogstyrd djupsammanslagning av lagrade JSON-inställningar som förhindrar krascher av saknade nycklar när schemat utvecklas.", "content_html": "<p><strong>Situation.</strong> Plattformen lagrar inställningar som JSON — aviseringsinställningar och liknande — i form av en användares sparade värden lagda ovanpå en uppsättning standardvärden. Den ursprungliga sammanslagningen gjorde det på en enda nivå. Problemet visar sig senare: lägg till en ny nyckel i standardvärdena, så saknar rader som sparades innan nyckeln fanns den helt enkelt. Sedan läser någon klientkod det fältet, får undefined och kraschar — för just de användare som har varit med längst.</p>\n<p><strong>Uppgift.</strong> Inställningsschemat behövde kunna utvecklas på ett säkert sätt, så att en ny inställning aldrig kunde ställa till det för de användare som registrerade sig innan den fanns.</p>\n<p><strong>Handling.</strong> Sammanslagningen på en nivå ersattes med en djup sammanslagning som styrs av standardvärdena som katalog. Standardvärdena behandlas som den auktoritativa listan över varje nyckel som ska finnas; användarens lagrade värden slås samman rekursivt ovanpå, så att allt som finns i katalogen garanterat finns med i resultatet, oavsett om det fanns i den sparade bloben eller inte. Lägg till en nyckel i standardvärdena, så dyker den automatiskt upp i de gällande inställningarna för varje befintlig rad, nästlade nycklar inräknade. Ändringen rullades ut genom en migrering, så att befintliga data fick nyttan direkt i stället för att vänta på att skrivas om.</p>\n<p><strong>Resultat.</strong> Inställningarna kan växa utan oro. En ny inställning riskerar inte längre en krasch i klienten på grund av ett fält som är undefined, och frontenden slapp behovet av defensiva kontroller utspridda på varje ställe där den läser en inställning. Det gav också resten av plattformen ett pålitligt sätt att utöka vilken lagrad JSON‑blob som helst — mönstret, inte bara den enskilda rättningen.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "Datastyrning", "Migrering och modernisering", "PostgreSQL", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Datastyrning och datakvalitet" ] }, { "id": "https://engineer.company/sv/portfolio/modeled-the-maritime-domain-professionals-companies-ships-jobs-67/", "url": "https://engineer.company/sv/portfolio/modeled-the-maritime-domain-professionals-companies-ships-jobs-67/", "title": "Modellerade sjöfartsdomänen i 348 normaliserade tabeller i 34 PostgreSQL‑scheman — yrkesverksamma, företag, fartyg, jobb, recensioner och resten — med SMALLINT‑uppslag och UUID v7‑nycklar.", "summary": "Modellerade sjöfartsdomänen i 348 normaliserade tabeller i 34 PostgreSQL-scheman, med SMALLINT-uppslag, UUID v7-nycklar och en enda namnkonvention.", "content_html": "<p><strong>Situation.</strong> Kärnan i plattformen är en tät maritim domän — yrkesverksamma, företag, fartyg, jobb, recensioner — och de här sakerna refererar ständigt till varandra. En yrkesverksam seglar på fartyg, arbetar för företag och lämnar recensioner; ett företag äger fartyg och publicerar jobb. Nästan varje funktion är en databasfråga över det nätet, så hur väl data modelleras avgör prestandan i större delen av appen och hur rimligt det är att bygga ut den.</p>\n<p><strong>Uppgift.</strong> Domänen behövde modelleras så att den förblev snabb och behöll sin integritet, och så att nästa entitetstyp kunde läggas till utan att någon behövde kämpa mot schemat.</p>\n<p><strong>Handling.</strong> Den är uppbyggd som normaliserade PostgreSQL‑scheman organiserade per domän — vid det här laget 348 tabeller fördelade på 34 av dem. De många små, stabila uppräkningarna — statusar, typer, kategorier — blev uppslagstabeller med SMALLINT, vilket håller raderna kompakta och joinarna billiga i stället för att textkoder lagras överallt. Entiteterna får primärnycklar med UUID v7, så att de är globalt unika men ändå tidsordnade i indexet. En och samma namnkonvention gäller genomgående — tabellnamn i plural inuti scheman med namn i singular — och tillämpas utan undantag, vilket som en bonus undviker en mängd kollisioner med reserverade ord. Och relationerna hålls uppe av riktiga främmande nycklar och villkor, så att integriteten är databasens uppgift och inte något som applikationen måste komma ihåg att sköta.</p>\n<p><strong>Resultat.</strong> Det blev en datamodell som är konsekvent och snabb, och förutsägbar att arbeta i eftersom samma regler gäller överallt — det finns inga specialfall att lägga på minnet. Att lägga till en funktion innebär oftast att schemat byggs ut längs de befintliga linjerna i stället för mot dem, vilket är det enda skälet till att 34 scheman är en struktur och inte ett spretigt virrvarr. Det är golvet som resten av plattformen står på.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Databaser", "Datastyrning", "Plattformsarkitektur", "PostgreSQL", "Prestandaoptimering", "SQL", "Databasdesign och datamodellering", "Datastyrning och datakvalitet", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/provisioned-azure-infrastructure-as-code-with-bicep-container-68/", "url": "https://engineer.company/sv/portfolio/provisioned-azure-infrastructure-as-code-with-bicep-container-68/", "title": "Provisionerade Azure‑infrastruktur som kod med Bicep — Container Apps, PostgreSQL Flexible Server, Front Door/WAF och nätverk — i utvecklings-, staging- och produktionsmiljöerna.", "summary": "Satte upp Azure-infrastruktur som kod med Bicep — Container Apps, PostgreSQL, Front Door/WAF — reproducerbar i dev, staging och prod.", "content_html": "<p><strong>Situation.</strong> Plattformen ligger i Azure, och Azure för hand — att klicka sig igenom portalen och justera en inställning här och där — är en fälla. Det glider iväg, ingen minns varför något är som det är, och att bygga upp det igen efter en dålig dag går långsamt och sliter på nerverna. Med mer än en miljö att hålla i takt blir det bara värre.</p>\n<p><strong>Uppgift.</strong> Allt skulle in i kod, så att en miljö blir något du kan läsa, granska och återskapa i stället för en hög med manuellt skapat tillstånd.</p>\n<p><strong>Handling.</strong> Hela uppsättningen är definierad i Bicep. Varje miljö — testing, staging, product — kommer ur samma mallar: api och www som körs som Azure Container Apps i en hanterad miljö, en PostgreSQL Flexible Server, Redis för cachning, Front Door med en WAF‑policy längst fram och nätverket under alltihop (VNet, NSG, privat DNS), med Log Analytics inkopplat för diagnostik. Containeravbildningarna hämtas från projektets Azure Container Registry. Eftersom allt är parametriserat är det en pull request att sätta upp en ny miljö eller ändra en befintlig, inte ett supportärende till dig själv.</p>\n<p><strong>Resultat.</strong> Miljöerna blev reproducerbara och möjliga att granska. Avvikelser slutade vara ett mysterium, eftersom sanningskällan är koden, och att få upp infrastrukturen eller återställa den handlar om att tillämpa mallarna i stället för att minnas vad som klickades fram förra gången. Det är skillnaden mellan infrastruktur som du äger och infrastruktur som äger dig.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Molntjänster", "Nätverk och VPN", "Plattformsarkitektur", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Infrastruktur som kod", "Molninfrastruktur och migrering", "Nätverk och VPN‑uppsättning", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/built-github-actions-ci-cd-pipelines-with-a-69/", "url": "https://engineer.company/sv/portfolio/built-github-actions-ci-cd-pipelines-with-a-69/", "title": "Byggde CI/CD‑pipelines i GitHub Actions med en distroless‑image för frontenden i produktion och uppflyttning mellan flera miljöer.", "summary": "Byggde CI/CD i GitHub Actions med en distroless-image för frontenden i produktion och flytt mellan flera miljöer, så att releaser blev odramatisk rutin.", "content_html": "<p><strong>Situation.</strong> Att leverera borde inte hänga på att någon kommer ihåg stegen, och det som till slut körs i produktion borde inte vara en fet allmän container som bär på ett skal och en pakethanterare som den aldrig kommer att använda — det är bara attackyta som ligger där utan anledning.</p>\n<p><strong>Uppgift.</strong> Vägen från commit till körning i Azure skulle bli automatisk, och produktionsavbildningarna skulle hållas så små och låsta som varje arbetslast tillåter.</p>\n<p><strong>Handling.</strong> Pipelinen är GitHub Actions. Separata arbetsflöden hanterar kodkvalitetsgrinden, testerna och driftsättningarna per miljö, med CodeQL, beroendegranskning och ett SBOM‑steg vid sidan av, så att ingenting når en miljö utan att först ha passerat kontrollerna. Avbildningarna byggs i flera steg, och basen för varje del valdes efter sina egna meriter i stället för som ett enda generellt val: frontenden levereras på en distroless‑avbildning (gcr.io/distroless/cc‑debian13 — inget skal, ingen pakethanterare), Go‑API:et på en slimmad Alpine och databasavbildningen på postgres‑slim. Ett bygge förs vidare genom miljöerna längs en fastställd väg i stället för manuellt.</p>\n<p><strong>Resultat.</strong> Releaser slutade vara en försiktig manuell ritual och blev en rutinmässig, tråkig händelse, vilket är precis vad man vill ha av releaser. Frontenden i produktion körs på ungefär så lite som det går att ge den, kontrollerna fångar problem innan de landar, och ”driftsättning” är något som pipelinen gör snarare än något som någon svettas sig igenom.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "DevOps", "Molntjänster", "Säkerhet", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Containerisering och orkestrering", "DevOps och CI/CD‑automatisering" ] }, { "id": "https://engineer.company/sv/portfolio/authored-578-go-task-automation-targets-70/", "url": "https://engineer.company/sv/portfolio/authored-578-go-task-automation-targets-70/", "title": "Skrev 578 automationsmål i go‑task som täcker utvecklingslägena native, Docker och HTTPS samt lintning, testning, databas och driftsättning.", "summary": "Skrev 578 automatiseringsmål i go-task för lägena native, Docker och HTTPS — lintning, test, databas och driftsättning bakom en enda verktygskedja.", "content_html": "<p><strong>Situation.</strong> Kodbasen är ett polyglott monorepo — Go, TypeScript, SQL, Python, skalskript — och vart och ett av språken har sitt eget sätt att bygga, testa, linta och köra. Lämnat åt sig självt innebär det att alla bär på en mental fusklapp med verktygsspecifika kommandon, och att nykomlingar ägnar sin första dag åt att bara lista ut hur man får saker att gå.</p>\n<p><strong>Uppgift.</strong> Hela projektet behövde en enda ingång: ett enda, konsekvent sätt att köra vad som helst, oavsett vilket språk det råkar vara skrivet i.</p>\n<p><strong>Handling.</strong> Det är uppbyggt med go‑task — ett Taskfile‑lager som har vuxit till 578 namngivna mål, 50 i rotfilen och 528 i filer med egna namnrymder under den. Där finns utvecklingslägena (nativt, Docker, en HTTPS‑variant för att testa PWA och mobil), kodkvalitetssidan (lint, formatering, test, fix för alla språk), databashanteringen och de miljöspecifika uppgifterna för bygge och driftsättning. Det finns till och med ett läge för lite minne, för maskiner som inte har RAM över för att bygga frontenden på vanligt sätt. Poängen var aldrig att ha många uppgifter; poängen var att du aldrig ska behöva känna till det underliggande kommandot.</p>\n<p><strong>Resultat.</strong> Vem som helst kan köra task –list och se hela verktygskedjan utlagd, och köra vilken del som helst av den på samma sätt oavsett vad som finns under huven. Onboardingen blev kortare, och de små, dumma misstagen — fel flagga, fel katalog, ett halvt ihågkommet kommando — försvann för det mesta. Antalet är också en varning: 578 mål är mer än någon kan hålla i huvudet, så det är namngivningen och namnrymderna som håller det användbart, inte antalet som skulle vara något att vara stolt över.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Tekniskt ledarskap", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/owned-end-to-end-deployments-of-the-platform-71/", "url": "https://engineer.company/sv/portfolio/owned-end-to-end-deployments-of-the-platform-71/", "title": "Ansvarade för plattformens driftsättningar till Azure från början till slut och hanterade releaser i utvecklings-, staging- och produktionsmiljöerna.", "summary": "Hade helhetsansvar för driftsättningarna i Azure och förde releaser genom dev, staging och produktion längs en definierad, repeterbar väg.", "content_html": "<p><strong>Situation.</strong> Plattformen behövde nå användare i flera Azure‑miljöer, och driftsättningen är skarven där infrastrukturen, byggpipelinen och applikationen möts. Det är också där ett litet misstag slutar vara en bugg och blir ett avbrott, så det är den del man allra minst vill sköta för hand och halvt ur minnet.</p>\n<p><strong>Uppgift.</strong> Driftsättningen ägdes från början till slut, så att en ändring rullades ut till varje miljö på samma förutsägbara sätt varje gång.</p>\n<p><strong>Handling.</strong> Releaser följer en fast väg — utveckling, sedan staging, sedan produktion — i stället för att någon pushar direkt till en skarp miljö. CI/CD‑pipelinen bygger avbildningarna och levererar dem, och Bicep‑mallarna håller målinfrastrukturen identisk från en miljö till nästa, så att ett bygge inte förs vidare till en subtilt annorlunda plats varje gång. Konfiguration som skiljer sig mellan miljöerna hålls åtskild från hemligheterna, vilket innebär att samma byggda artefakt kan föras vidare genom miljöerna och helt enkelt plockar upp rätt inställningar där den hamnar, i stället för att byggas om för var och en.</p>\n<p><strong>Resultat.</strong> Ändringar når varje miljö förutsägbart, längs en fastställd väg, utan några manuella ad hoc‑driftsättningar inblandade. Att släppa en release blev ett kontrollerat, upprepbart steg i stället för ett ögonblick med hållen andning, och det är en stor del av det som höll den skarpa plattformen stabil medan den fortfarande förändrades snabbt under ytan.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Molntjänster", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Molninfrastruktur och migrering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/configured-database-backup-retention-as-infrastructure-as-code-72/", "url": "https://engineer.company/sv/portfolio/configured-database-backup-retention-as-infrastructure-as-code-72/", "title": "Konfigurerade lagringstiden för databasens säkerhetskopior som infrastruktur som kod, granskade sedan återställningsförmågan och dokumenterade rutinen för återläsning — och pekade ut de återstående luckorna i stället för att låta dem upptäckas under en incident.", "summary": "Införde säkerhetskopiering av databaser och en strategi för katastrofåterställning som infrastruktur som kod, för snabb och reproducerbar återställning.", "content_html": "<p><strong>Situation.</strong> En produkt som lever på sina data har inte råd att förlora några, och ”det finns säkerhetskopior någonstans” är en förhoppning, inte en återställningsplan. Den enda säkerhetskopia som är värd att ha är en som du vet går att återställa, till en miljö som du vet att du kan bygga upp igen. Produkten hade ingen av de två halvorna nedskriven.</p>\n<p><strong>Uppgift.</strong> Återställningsförmågan skulle definieras i stället för att tas för given — data, miljön runt dem och en ärlig redogörelse för hur långt det faktiskt räcker i dag.</p>\n<p><strong>Handling.</strong> Lagringstiden för säkerhetskopior är konfigurerad i Bicep bredvid databasen som de skyddar, så att återställning till valfri tidpunkt är en egenskap hos mallen i stället för en inställning som någon en gång klickade fram i en portal. Miljön runt den är också definierad som infrastruktur som kod, vilket är den tysta halvan som folk glömmer: att återställa en databas till en miljö som du skulle behöva bygga upp igen för hand ur minnet är egentligen ingen återställning. Därefter granskades och dokumenterades läget — återställningsproceduren, övningen som skulle mäta den och de luckor som fortfarande är öppna: georedundans är avslagen, och målet för återställningstid är föreslaget snarare än uppmätt, eftersom ingen övning har genomförts än.</p>\n<p><strong>Resultat.</strong> Återställning slutade vara en vag försäkran och blev ett dokumenterat läge med luckorna namngivna. Det låter mindre imponerande än ”katastrofåterställning: klar”, och det är värt betydligt mer — den som rör det härnäst vet vad som är täckt, vad som inte är det och exakt vilken övning som täpper till skillnaden. En namngiven lucka går att täppa till; en onamngiven upptäcks under en incident.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Databaser", "DevOps", "Infrastruktur", "Molntjänster", "PostgreSQL", "Tillförlitlighet och säkerhetskopiering", "Databasadministration (DBA)", "Infrastruktur som kod", "Säkerhetskopiering och återställning" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-next-js-16-frontend-with-deliberate-73/", "url": "https://engineer.company/sv/portfolio/built-the-next-js-16-frontend-with-deliberate-73/", "title": "Byggde frontenden i Next.js 16 med medvetna SSR-, SSG- och CSR‑strategier och en återanvändbar fabrik för förhämtade serversidor som tar bort N+1‑kaskaden av hämtningar från varje inloggad sida som flyttas över till den.", "summary": "Byggde frontenden i Next.js 16 med SSR/SSG/CSR och ett mönster för förhämtning och hydrering i serverskalet som eliminerade N+1-anrop.", "content_html": "<p><strong>Situation.</strong> Instrumentpanelen för inloggade användare behövde kännas snabb och förbli genuint interaktiv, och de två målen drar åt olika håll om man är naiv. Hämta allt på klienten, så drar den första laddningen ut på tiden, och värre än så får du N+1‑mönstret där varje komponent vaknar och skickar sitt eget anrop, så att en enda sida blir en kaskad av rundresor.</p>\n<p><strong>Uppgift.</strong> Varje del av appen behövde renderas på det sätt som faktiskt passade den, utan att interaktiviteten på klientsidan offrades där den spelade roll.</p>\n<p><strong>Handling.</strong> Frontenden i Next.js 16 använder rätt läge för varje yta i stället för ett enda generellt val. Marknadsföringssidor och publika sidor genereras statiskt — de ändras inte per användare, så det finns ingen anledning att rendera dem vid varje anrop. De genuint interaktiva delarna förblir klientrenderade. Och där N+1‑kaskaden faktiskt biter — dashboarden för inloggade användare och de stora katalogsidorna — bröts lösningen ut i en gemensam fabrik i stället för att skrivas för hand: createPrefetchedServerPage hämtar sidans data på servern och lämnar över dem till klienten redan ifyllda, så att komponenterna startar med sina data i stället för att var och en ger sig iväg för att be om dem. En strategi för cacheinvalidering är dokumenterad per datahämtning, så att data hålls färska utan att appen hämtar om sådant den redan har.</p>\n<p><strong>Resultat.</strong> Sidorna som har flyttats till fabriken laddas med en enda rundresa i stället för en storm av dem, och eftersom det är en fabrik och inte ett mönster som folk kopierar för hand ärver nästa sida som flyttas över beteendet utan extra arbete. Resten av den inloggade appen är fortfarande klientrenderad och väntar fortfarande på sin tur efter den — en migrering med en fungerande mekanism och en självklar nästa sida, snarare än en avslutad genomgång.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Frontendutveckling", "Fullstackutveckling", "Plattformsarkitektur", "Prestandaoptimering", "Webbutveckling", "Fullstackutveckling av produkter", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/delivered-full-progressive-web-app-support-installable-and-74/", "url": "https://engineer.company/sv/portfolio/delivered-full-progressive-web-app-support-installable-and-74/", "title": "Levererade fullt stöd för Progressive Web App — installerbar och med offlinestöd — med körtidscachning i Workbox via next‑pwa.", "summary": "Levererade fullt stöd för Progressive Web App — installerbar och offlinekapabel — med körtidscachning i Workbox via next-pwa.", "content_html": "<p><strong>Situation.</strong> Många av plattformens användare är till sjöss. Sjöfolk och fältpersonal på mobilen, ofta offline eller hängande på en dålig uppkoppling — inte folk som sitter vid ett skrivbord med pålitligt kontors‑wifi. Att bygga som om alla hade ett snabbt och konstant nätverk hade i det tysta stängt ute en stor del av den faktiska publiken.</p>\n<p><strong>Uppgift.</strong> Appen behövde gå att installera som en nativ app och fortfarande gå att använda när nätet försvinner.</p>\n<p><strong>Handling.</strong> Den levereras som en fullfjädrad Progressive Web App. Den installeras på hemskärmen med ordentliga ikoner i de olika storlekarna, en startskärm och temafärger, så att den ser ut och startar som en app och inte som ett bokmärke. Service workern sätts upp via pluginet next‑pwa, och Workbox cachar vid körning med CacheFirst för det som inte ändras per anrop — typsnitt, bilder, ljud, video, CDN‑resurser — tillsammans med rimliga HTTP‑huvuden för cachning. Eftersom service workers egentligen bara fungerar som de ska över HTTPS validerades beteendet offline och vid installation genom testning över HTTPS på riktiga enheter i stället för att det bara antogs fungera.</p>\n<p><strong>Resultat.</strong> Användarna kan installera appen och fortsätta använda den offline, och upprepade laddningar kommer snabbt tillbaka från cachen i stället för över nätet. Plattformen beter sig som en nativ app på den hårdvara som användarna faktiskt bär med sig, vilket för den här publiken inte är något som bara är trevligt att ha — det är skillnaden mellan att appen går att använda till sjöss och att den inte gör det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Frontendutveckling", "Fullstackutveckling", "Prestandaoptimering", "Tillförlitlighet och säkerhetskopiering", "Webbutveckling", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/designed-an-ios-26-liquid-glass-design-system-75/", "url": "https://engineer.company/sv/portfolio/designed-an-ios-26-liquid-glass-design-system-75/", "title": "Utformade ett designsystem i stil med ”Liquid Glass” från iOS 26 och en kanonisk komponentförteckning som lintregler upprätthåller för att förhindra att gränssnittet spretar.", "summary": "Utformade ett designsystem i ”Liquid Glass”-stil för iOS 26 och en kanonisk komponentförteckning som lintregler upprätthåller för att stoppa avvikelser i UI.", "content_html": "<p><strong>Situation.</strong> Ett gränssnitt utan ett gemensamt visuellt språk och en fast uppsättning komponenter glider isär, och det går fort. Varje ny skärm uppfinner sina egna knappar, märken och kort, alla lite olika, och de små inkonsekvenserna hopar sig tills produkten ser osammanhängande ut och varje ändring innebär att fem skräddarsydda versioner av samma sak måste röras.</p>\n<p><strong>Uppgift.</strong> Designsystemet behövde vara tillräckligt sammanhängande för att se avsiktligt ut och tillräckligt lätt att upprätthålla för att inte vittra sönder så fort teamet hade mycket att göra.</p>\n<p><strong>Handling.</strong> Det visuella språket och komponentsystemet utformades som en helhet. Språket är en look i stil med ”Liquid Glass” i iOS 26 — genomskinliga paneler med bakgrundsoskärpa, skikt av skuggor, lite ljusbrytning, fjäderanimationer — uppbyggd av Tailwind‑verktygsklasser. Ovanpå den ligger en kanonisk uppsättning komponenter som varje skärm är tänkt att byggas av: Card, Label, Button, GlassIconButton, DirectoryGrid och resten. Det som får det att fastna är lintningen: regler som avvisar ett handbyggt märke eller chip och pekar dig mot den kanoniska komponenten i stället, så att systemet hålls uppe av verktyg och inte av att den som granskar just den dagen kommer ihåg att bry sig. Och dokumentationen kompletteras med konkreta anteckningar som går från misstag till lösning, så att vägledningen är ”så här gör man fel och så här gör man rätt” och inte en abstrakt princip.</p>\n<p><strong>Resultat.</strong> Gränssnittet förblir konsekvent och troget varumärket, och de engångskomponenter som annars skulle sprida sig fångas innan de hinner göra det. Visuell konsekvens slutade vara en fråga om allas disciplin och blev något som verktygen står vakt om, vilket är den enda versionen av det som överlever en deadline.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Dokumentation", "Frontendutveckling", "UX/UI‑design", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/designed-the-product-s-ui-and-ux-end-76/", "url": "https://engineer.company/sv/portfolio/designed-the-product-s-ui-and-ux-end-76/", "title": "Utformade produktens UI och UX från början till slut — katalogrutnät, dubbla kort- och tabellvyer, kravvalidering i realtid och brödsmulenavigering.", "summary": "Utformade produktens UI/UX hela vägen — katalograster, kort- och tabellvyer, live-validering och brödsmulor — för täta sjöfartsdata.", "content_html": "<p><strong>Situation.</strong> Plattformen visar många olika slags entiteter för människor — yrkesverksamma, företag, fartyg, jobb, recensioner — och gränssnittet måste vara två saker som strider mot varandra: snyggt och genuint användbart. Tätt nog för att visa verkliga maritima data, men inte så tätt att det blir en vägg som man studsar mot.</p>\n<p><strong>Uppgift.</strong> Produktens UI och UX ägdes från början till slut — layouterna, interaktionsmönstren och de mindre sakerna, som hur någon guidas genom ett formulär utan att känna sig tjatad på.</p>\n<p><strong>Handling.</strong> Några få beslut gjorde det mesta av jobbet. Katalogrutnäten har en växling mellan kort och tabell, så att du kan bläddra visuellt eller skumma en tät tabell, och appen kommer ihåg vad du valde. Formulären har kravkontroller i realtid som sitter ovanför fältet och visar varje regel i grönt när den är uppfylld och i gult när den inte är det, så att du får vägledning medan du skriver i stället för att bli tillrättavisad efter att du har skickat. Navigeringen består av konsekventa brödsmulor och tvåkolumnslayouter för entiteter, så att sidorna känns som samma produkt och inte som en samling orelaterade skärmar. Och filosofin för fel är vägledning framför misslyckande — inga röda väggar, omdirigeringar i stället för återvändsgränder, en app som försöker hålla dig i rörelse i stället för att stoppa dig.</p>\n<p><strong>Resultat.</strong> Det blev en genomarbetad, konsekvent upplevelse som gör täta maritima data lätta att ta till sig och lotsar människor genom de komplicerade delarna. Den uppfattas som genomtänkt och pålitlig, vilket inte är kosmetika på en plattform som folk använder för sina faktiska karriärer — om den såg slarvig ut skulle de lita mindre på uppgifterna, och de skulle ha rätt i det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Frontendutveckling", "Produkt och krav", "UX/UI‑design", "Webbutveckling", "Produktstrategi och krav", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/", "url": "https://engineer.company/sv/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/", "title": "Härdade applikationen med nonce‑baserad CSP, HSTS, SameSite‑cookies, databasroller med minsta behörighet och förnyade kontroller av åtkomsträttigheter på serversidan.", "summary": "Härdade appen med noncebaserad CSP, HSTS, SameSite-cookies, databasroller med minsta möjliga behörighet och omkontroll av rättigheter på serversidan.", "content_html": "<p><strong>Situation.</strong> Plattformen lagrar yrkes- och organisationsdata, den sortens data som folk förväntar sig ska hanteras ordentligt, så en enda försvarslinje skulle aldrig räcka. Arbetsantagandet måste vara att klienten är fientlig — att allt som webbläsaren upprätthåller kan kopplas bort av den som sitter vid webbläsaren — och säkerheten måste hålla ändå.</p>\n<p><strong>Uppgift.</strong> Plattformen behövde härdas i varje lager — frontend, API, databas — så att säkerheten upprätthölls av servern oberoende av vad gränssnittet råkade tillåta.</p>\n<p><strong>Handling.</strong> I frontenden sätter middlewaren i Next.js — <code>proxy.ts</code> — en Content‑Security‑Policy med en nonce per anrop och strict‑dynamic, plus HSTS och SameSite‑cookies, så att webbläsaren är låst när det gäller vad den kör och skickar. I API:et finns hastighetsbegränsning, CORS, storleksgränser för anrop, validering av indata innan något når databasen och loggning av säkerhetsrelevanta händelser. I databasen loggar API:et in som en roll med minsta möjliga behörighet som bara får anropa appens funktioner med EXECUTE, funktionerna körs som SECURITY DEFINER och allt är parametriserat. Och rättigheterna — nivå, roll, organisation, avgränsning per fartyg — kontrolleras på nytt på servern vid varje anrop, medan spärrarna i frontenden enbart behandlas som UX. Spärrarna avgör vad du ser; servern avgör vad du får göra.</p>\n<p><strong>Resultat.</strong> Säkerheten hänger inte på att gränssnittet beter sig. Skydden ligger i lager så att den som tar sig förbi ett av dem inte därmed tar sig förbi resten, och det hela bygger på antagandet att klienten inte går att lita på — vilket är rätt antagande för data som människor lämnar över i förtroende.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Databaser", "Frontendutveckling", "Plattformsarkitektur", "Säkerhet", "Backend- och API‑utveckling", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/established-an-english-danish-internationalization-system-with-linter-78/", "url": "https://engineer.company/sv/portfolio/established-an-english-danish-internationalization-system-with-linter-78/", "title": "Etablerade ett internationaliseringssystem för engelska och danska med 12 027 meddelanden per språkversion i 458 namnrymdsfiler, med ett ordförråd som lintern upprätthåller och en budget på 400 rader per fil.", "summary": "Etablerade ett i18n-system för engelska/danska med 12 027 meddelanden per språk i 458 filer, med lintstyrt ordförråd och en storleksbudget.", "content_html": "<p><strong>Situation.</strong> Plattformen finns på engelska och danska, och översättningssystem har en tendens att svälla ut till en enda röra. Filerna växer utan gräns, nycklar läcker — finns i en språkversion, saknas i den andra — och de språkspecifika konventionerna tillämpas ojämnt, så att en språkversion till slut låter som om den hade översatts av en kommitté vars ledamöter aldrig pratade med varandra. För en professionell produkt är det ingen liten skönhetsfläck; det uppfattas som slarv.</p>\n<p><strong>Uppgift.</strong> Internationaliseringen behövde skala — så att båda språken hölls konsekventa och korrekta, och så att filerna fortfarande var något som en människa kunde underhålla ett år senare.</p>\n<p><strong>Handling.</strong> Den bygger på next‑intl med regler som verktygen faktiskt upprätthåller. Den danska sidan har ett låst ordförråd och en låst stil — bokstavliga æøå, det informella ”du”, rätt accenttecken i imperativformer och semantiska mappningar för sammansatta substantiv, så att de översätts efter betydelse i stället för ord för ord — och en linter ser till att den håller sig till det. Det finns en hård budget på 400 rader per namnrymdsfil, med en metod för att dela upp och slå samman, så att ett stort område delas i nästlade filer som slås samman rent igen i stället för att en enda fil växer i all evighet; den budgeten är skälet till att 12 027 meddelanden per språkversion ligger i 458 filer i stället för i en handfull enorma. En kontroll i lintern jämför nycklarna mellan språkversionerna så att ingenting läcker eller försvinner. Och lagrade åsidosättningar slås samman utifrån katalogen i stället för med sköra reservvärden på toppnivå — samma idé om djup sammanslagning som används för inställningarna.</p>\n<p><strong>Resultat.</strong> Båda språkversionerna förblir korrekta och konsekventa med 24 054 meddelanden tillsammans, och filerna förblir underhållbara i takt med att antalet strängar ökar. Språkkvaliteten blev något som verktygen garanterar vid varje commit i stället för något som i det tysta försämras varje gång någon lägger till en sträng i all hast. Budgeten är den bärande delen: utan en gräns per fil hade 458 filer varit tolv, och ingen skulle öppna dem.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Frontendutveckling", "Fullstackutveckling", "Internationalisering", "Internationalisering och lokalisering" ] }, { "id": "https://engineer.company/sv/portfolio/set-a-zero-warnings-quality-bar-across-six-79/", "url": "https://engineer.company/sv/portfolio/set-a-zero-warnings-quality-bar-across-six-79/", "title": "Satte kvalitetsribban vid noll varningar i sex språk — Go, TypeScript, SQL, Python, Shell och Markdown — upprätthållen av pre‑commit‑hookar.", "summary": "Satte ett kvalitetskrav på noll varningar för Go, TypeScript, SQL, Python, Shell och Markdown, upprätthållet med pre-commit-hooks.", "content_html": "<p><strong>Situation.</strong> Varningar som hopar sig är tyst frätande. Varje diagnostikmeddelande som du ignorerar sänker ribban lite, och när bygget väl spottar ut fyrtio av dem läser ingen något av dem, och ett verkligt problem ligger fullt synligt i listan eftersom ”varningar” har blivit bakgrundsbrus. I en polyglott kodbas finns det så mycket fler källor till brus som låter det hända.</p>\n<p><strong>Uppgift.</strong> Repot behövde en enda kompromisslös kvalitetsribba för alla språk, så att saker åtgärdades i stället för att samlas på hög.</p>\n<p><strong>Handling.</strong> En nollvarningspolicy infördes, med verktygen som det som upprätthåller den, eftersom en policy som förlitar sig på allas vaksamhet förlorar mot den första hektiska veckan. Varje diagnostikmeddelande från en linter är ett fel — det finns ingen ”warn”-nivå att gömma sig i — och det gäller likadant i hela stacken: Go med golangci‑lint, TypeScript med ESLint, SQL med SQLFluff, Python med Ruff, skalskript med ShellCheck, Markdown med markdownlint. Undertryckningar direkt i koden är förbjudna, så det går inte att släta över ett diagnostikmeddelande; själva saken måste faktiskt åtgärdas. Hookar för pre‑commit och pre‑push kör linters och tester, så att en commit som skulle föra in ett problem aldrig blir gjord över huvud taget. Det finns till och med gränser för längden på funktioner och filer, för att hindra moduler från att svälla ut förbi gränsen för vad som går att läsa.</p>\n<p><strong>Resultat.</strong> Problem åtgärdas vid källan i stället för att skjutas upp till en backlog som ingen betar av, och kodbasen förblir ren som standard snarare än genom återkommande hjältedåd. Standarden är identisk oavsett vilket språk du arbetar i, och det är verktygen som håller den — inte någons viljestyrka — vilket är skälet till att den faktiskt håller.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Tekniskt ledarskap", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/", "url": "https://engineer.company/sv/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/", "title": "Fattade plattformens grundläggande beslut under veckorna efter att repot öppnades i november 2025 — skiktningen, dataåtkomst med databasen först och ribban med noll varningar — och de håller fortfarande nio månader senare.", "summary": "Lade fast plattformens grundbeslut i november 2025 — skiktningen, databascentrerad dataåtkomst och kravet på noll varningar — och de gäller än i dag.", "content_html": "<p><strong>Situation.</strong> Repot skapades den 25 november 2025, helt tomt. Det som avgörs under de första veckorna i ett sådant projekt får oproportionerligt stor betydelse: lagerindelningen, var affärslogiken får bo, vilken kvalitetsribban är. De valen är billiga att göra dag tre och nästan omöjliga att ändra på vid månad sex, då allt som har skrivits sedan dess förutsätter dem.</p>\n<p><strong>Uppgift.</strong> Grundbesluten behövde fattas medvetet och tidigt, och i en form som tålde att lämnas över till andra människor och till en mycket större kodbas än den som fanns då.</p>\n<p><strong>Handling.</strong> Tre beslut gjorde det mesta av jobbet. Stacken delades upp i lager så att varje del har en enda uppgift — en frontend i Next.js, ett Go‑API som validerar och skickar vidare, ett funktionslager i PostgreSQL som äger affärsreglerna — i stället för att logiken placerades där det råkade passa just den eftermiddagen. Dataåtkomsten lades bakom lagrade funktioner från början, vilket är det val som allt annat i databasarbetet följer av; att bygga in det i efterhand hade inneburit att varje hanterare skrevs om. Och en ribba med noll varningar infördes innan det fanns mycket kod att hålla till den, eftersom en standard som införs vid commit 5 000 är ett städprojekt, medan samma standard vid commit 50 helt enkelt är hur repot fungerar. Inget av de tre var det enkla alternativet då, och alla tre kostade fart under den första månaden.</p>\n<p><strong>Resultat.</strong> Nio månader och flera tusen commits senare håller alla tre fortfarande: lagren har inte flutit ihop, ingen hanterare pratar direkt med en tabell, och bygget har fortfarande inga varningar. Det är den kontroll som är värd att göra av ett grundbeslut — inte om det lät rätt, utan om det överlevde mötet med den arbetsmängd som kom efteråt, vilket är den punkt där bekväma val brukar överges i det tysta.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "DevOps", "Frontendutveckling", "Fullstackutveckling", "Lösningsarkitektur", "Plattformsarkitektur", "Säkerhet", "Teamledning", "Tekniskt ledarskap", "Backend- och API‑utveckling", "Fullstackutveckling av produkter", "Plattforms- och lösningsarkitektur", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/", "url": "https://engineer.company/sv/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/", "title": "Levererade analyser och regelbundna lägesrapporter till vd:n och omsatte tekniska nyckeltal och leveransstatus i beslut.", "summary": "Levererade analyser och regelbundna lägesrapporter till vd:n och omsatte tekniska mätvärden och leveransstatus i säkra beslut.", "content_html": "<p><strong>Situation.</strong> I takt med att plattformen tog form behövde dess framsteg och hälsa bli synliga för ledningen i termer som den faktiskt kunde agera på. Råa tekniska signaler — byggstatus, leveranstakt, incidenter — säger inte mycket i sig för någon som fattar beslut om produkt och investeringar; de ligger på fel höjd. Någon behövde översätta.</p>\n<p><strong>Uppgift.</strong> Den översättningen var uppgiften: att ta den tekniska verkligheten — var leveransen stod, vad systemmåtten sade — och rapportera den till vd:n tydligt och regelbundet, så att besluten vilade på fakta i stället för gissningar.</p>\n<p><strong>Handling.</strong> En fast rapporteringsrytm infördes, i stället för rapportering på begäran, som alltid kommer lite för sent. Leveransframsteg, omfattning, risker och systemhälsa följdes upp och omvandlades till tydliga, beslutsorienterade uppdateringar — vad som ligger i fas, vad som är i riskzonen och vad en viss prioritering faktiskt skulle kosta i avvägningar. Det som skulle undvikas var att lämna över råa siffror och överlåta tolkningen till någon utan sammanhanget; varje rapport kom med konkreta rekommendationer, och där det hjälpte underbyggdes berättelsen med den underliggande analysen, så att ledningen kunde gräva djupare om den ville i stället för att behöva ta det på förtroende.</p>\n<p><strong>Resultat.</strong> Ledningen fick en tydlig, ärlig och aktuell bild av utvecklingsarbetet och kunde styra produktprioriteringar och investeringar med viss tillförsikt i stället för att flyga i blindo. Rapporteringen slutade vara en statusritual som ingen läser och blev något som beslut faktiskt fattades utifrån — vilket höll det tekniska arbetet och affärsinriktningen på samma kurs.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dataanalys", "Intressenter och rapportering", "Produkt och krav", "Projektledning", "Tekniskt ledarskap", "Dataanalys och BI‑dashboards", "Projektledning (agil)", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/provided-round-the-clock-24-7-infrastructure-support-82/", "url": "https://engineer.company/sv/portfolio/provided-round-the-clock-24-7-infrastructure-support-82/", "title": "Gav infrastruktursupport dygnet runt, 24/7, för en IPTV/OTT‑strömningsplattform och administrerade ca 1 000 servrar samt kundägda system för globala kunder i Kina, USA och Tyskland.", "summary": "Gav infrastruktursupport 24/7 för en IPTV/OTT-streamingplattform — ~1 000 servrar plus kundsystem i Kina, USA och Tyskland.", "content_html": "<p><strong>Situation.</strong> Det här var en strömningsplattform för IPTV/OTT med kunder spridda över Kina, USA och Tyskland, vilket innebar att det inte fanns någon lugn timme att göra underhåll på — någon, någonstans, tittade alltid. Driftstopp på en sådan plattform är inget abstrakt mått; det är någons tv som bara stannar, och den personen bryr sig inte om varför.</p>\n<p><strong>Uppgift.</strong> Att hålla plattformens infrastruktur tillgänglig dygnet runt var uppgiften — verkligen dygnet runt, inte ”kontorstid plus ett jourschema som ingen svarar på”.</p>\n<p><strong>Handling.</strong> 24/7‑supporten omfattade ungefär tusen servrar, plus ett stort antal kundägda system därtill — som administrerades, övervakades och hölls säkrade och konfigurerade i hela strömningsmiljön. Eftersom kunderna fanns i tre mycket olika tidszoner fanns ”efter kontorstid” egentligen inte; ett problem kl. 3 på natten lokal tid var bästa sändningstid för någon annan, så det behandlades som bästa sändningstid. En stor del av arbetet bestod i att märka när något började glida innan det blev ett avbrott, eftersom man på en direktsänd strömningsplattform inte får chansen att rätta till saker i lugn och ro i efterhand.</p>\n<p><strong>Resultat.</strong> Plattformen förblev oavbrutet tillgänglig för en global publik, med problem som fångades upp och hanterades vid vilken timme de än dök upp, innan de nådde en tittares skärm. För en tjänst som går 24/7 är det hela jobbet — framgång ser ut som att ingenting händer, vilket är precis vad tittarna ville ha.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Molntjänster", "Nätverk och VPN", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "IT‑support och helpdesk", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/ensured-uninterrupted-delivery-of-iptv-streaming-signals-between-83/", "url": "https://engineer.company/sv/portfolio/ensured-uninterrupted-delivery-of-iptv-streaming-signals-between-83/", "title": "Säkerställde oavbruten leverans av strömmade IPTV‑signaler mellan leverantörer och kunder och övervakade och underhöll strömningsnätet och IP‑telefonin dygnet runt.", "summary": "Säkrade oavbruten IPTV-streaming mellan leverantörer och kunder genom att övervaka och underhålla streamingnätet och IP-telefonin 24/7.", "content_html": "<p><strong>Situation.</strong> IPTV står och faller med att signalen går fram. Strömmarna flödar från leverantörer, genom plattformen, till kunderna, och varje avbrott var som helst i den kedjan är en svart skärm för någon. IP‑telefonin fanns vid sidan av, med samma krav: den skulle helt enkelt fungera.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att garantera att signalleveransen och telefonin förblev oavbrutna.</p>\n<p><strong>Handling.</strong> Strömningsnätet och IP‑telefonin övervakades, felsöktes och underhölls dygnet runt. Poängen med att bevaka det hela tiden är att strömningsproblem visar sig som försämring innan de blir ett rent avbrott — en ström som börjar hacka, en länk som blir instabil — och om man är uppmärksam kan man fånga det vid hackandet i stället för vid den svarta skärmen. Så en stor del av arbetet gick ut på att ligga steget före signalen i stället för att reagera på klagomål om den.</p>\n<p><strong>Resultat.</strong> Strömmarna och samtalen förblev tillförlitliga i hela plattformen, med problem som upptäcktes och åtgärdades innan någon märkte att tjänsten föll bort. Att hålla en signal flödande mellan leverantörer och slutkunder utan synliga glapp är ett tyst, ständigt arbete, och tyst är precis vad det ska vara.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Nätverk och VPN‑uppsättning", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/developed-and-maintained-django-web-applications-for-the-84/", "url": "https://engineer.company/sv/portfolio/developed-and-maintained-django-web-applications-for-the-84/", "title": "Utvecklade och underhöll Django‑webbapplikationer för IPTV‑plattformen, levererade nya funktioner och förbättrade prestanda och stabilitet.", "summary": "Utvecklade och underhöll Django-webbapplikationer för en IPTV-plattform och levererade funktioner samtidigt som prestanda och stabilitet förbättrades.", "content_html": "<p><strong>Situation.</strong> IPTV‑plattformen hade webbapplikationer byggda på Django runt sig — de delar som människor faktiskt klickade på — och de behövde fortsätta utvecklas: nya funktioner, och den prestanda och stabilitet som en plattform i drift dygnet runt kräver.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att hålla applikationerna funktionsrika, snabba och stabila.</p>\n<p><strong>Handling.</strong> Webbapplikationerna i Django utvecklades och underhölls — nya funktioner byggdes, buggar rättades och prestanda och stabilitet drevs framåt, i samarbete med hela teamet snarare än i ett hörn. På något som betjänar kunder utan avbrott är stabiliteten inte något trevligt att ha vid sidan av funktionerna; den är det villkor som funktionerna måste respektera. En flashig funktion som får det hela att vackla är inte värd mycket när det hela inte har råd att vackla.</p>\n<p><strong>Resultat.</strong> Applikationerna fick hela tiden nya förmågor och förblev samtidigt tillförlitliga för både de interna teamen och kunderna. Att bygga vidare på något utan att göra det instabilt var den balans som spelade roll här, och det var den som arbetet höll fast vid.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Frontendutveckling", "Fullstackutveckling", "Python", "Webbutveckling", "Backend- och API‑utveckling", "Fullstackutveckling av produkter", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/planned-and-implemented-new-infrastructure-functionality-for-internal-85/", "url": "https://engineer.company/sv/portfolio/planned-and-implemented-new-infrastructure-functionality-for-internal-85/", "title": "Planerade och implementerade ny infrastrukturfunktionalitet för interna och externa system och byggde lösningar som höll tillräckligt väl för att fortfarande vara i drift flera år senare med minimala ändringar.", "summary": "Planerade och byggde infrastruktur för interna och externa system, robust nog att fortfarande köras flera år senare med minimala ändringar.", "content_html": "<p><strong>Situation.</strong> I takt med att organisationen växte behövde dess interna och externa system hela tiden byggas på med nya funktioner. Det enkla sättet är att göra det som går snabbast i dag; problemet med det enkla sättet är att man står där och lagar det igen om sex månader.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att planera och bygga infrastrukturfunktionalitet som faktiskt skulle hålla — inte bara fungera nu, utan fortsätta att fungera.</p>\n<p><strong>Handling.</strong> Ny infrastrukturfunktionalitet planerades och implementerades i de interna och externa systemen, utformad för att vara hållbar — sådant man bygger en gång, ordentligt, så att det fortsätter att gå i flera år med minimal handpåläggning i stället för att kräva ständig uppmärksamhet. Det är ett medvetet val varje gång: att tänka efter lite mer från början, så att man inte binder upp sig för att passa det i all framtid.</p>\n<p><strong>Resultat.</strong> Systemen förblev funktionella och effektiva långt efter att de byggdes och gick i flera år nästan helt utan ändringar. Den livslängden är det verkliga måttet på infrastrukturarbete — vem som helst kan bygga något som fungerar i dag; att bygga något som fortfarande tyst fungerar flera år senare är både svårare och mer användbart.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Lösningsarkitektur", "Plattformsarkitektur", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Plattforms- och lösningsarkitektur", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/as-one-of-the-first-hires-designed-and-86/", "url": "https://engineer.company/sv/portfolio/as-one-of-the-first-hires-designed-and-86/", "title": "Utformade och byggde, som en av de första anställda, hela kärninfrastrukturen och de stödjande processerna från noll för en SaaS‑startup inom grön energi och lade grunden för snabb tillväxt.", "summary": "Utformade och byggde som en av de första anställda hela kärninfrastrukturen för en SaaS-startup inom grön energi från grunden och möjliggjorde snabb tillväxt.", "content_html": "<p><strong>Situation.</strong> Det här var ett nystartat SaaS‑bolag inom grön energi, med en lovande idé och i praktiken ännu ingen teknisk grund under sig. Att komma in som en av de första anställda innebar att hamna i skedet där det inte finns något att underhålla eftersom ingenting finns ännu — att lägga det golv som alla andra ska stå på.</p>\n<p><strong>Uppgift.</strong> Uppdraget var att bygga kärninfrastrukturen och processerna runt den, från grunden.</p>\n<p><strong>Handling.</strong> Hela kärninfrastrukturen och de stödjande processerna utformades och byggdes — servrarna, nätverken, dataflödet, säkerheten, driftsidan. Att göra det i ett nystartat bolag innebär att fatta beslut som är svåra att ändra senare, så målet var inte bara att ”få igång något”, utan att lägga en grund som kunde bära tyngden av snabb tillväxt utan att behöva rivas ut i samma stund som bolaget blev större. Tidiga infrastrukturval blir antingen det som gör att man kan skala upp eller det man lägger ett år på att riva upp; siktet var helt inställt på det första.</p>\n<p><strong>Resultat.</strong> Bolaget fick med sig en stabil teknisk grund, och det var den som gjorde att verksamheten kunde växa snabbt efteråt. Att vara den som bygger den basen från ingenting är en särskild sorts ansvar — gör man rätt märker ingen något, gör man fel märker alla det — och den här höll.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "DevOps", "Infrastruktur", "Lösningsarkitektur", "Molntjänster", "Plattformsarkitektur", "Systemadministration", "Säkerhet", "DevOps och CI/CD‑automatisering", "Molninfrastruktur och migrering", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/maintained-and-enhanced-the-legacy-hugo-static-site-87/", "url": "https://engineer.company/sv/portfolio/maintained-and-enhanced-the-legacy-hugo-static-site-87/", "title": "Underhöll och vidareutvecklade den äldre statiska webbplatsen i Hugo och bidrog samtidigt med UI/UX‑förbättringar till den huvudsakliga produkten för tillgångsförvaltning.", "summary": "Underhöll och förbättrade en äldre statisk webbplats i Hugo och bidrog med UI/UX-förfiningar till kärnprodukten för tillgångsförvaltning.", "content_html": "<p><strong>Situation.</strong> Det fanns en äldre webbplats byggd med Hugo — en generator för statiska webbplatser — som drevs parallellt med företagets huvudprodukt, ett system för tillgångshantering. Webbplatsen var det gamla, etablerade; produkten var där det verkliga värdet låg.</p>\n<p><strong>Uppgift.</strong> Uppdraget var att hålla webbplatsen i gott skick och samtidigt förbättra upplevelsen i produkten.</p>\n<p><strong>Handling.</strong> Den statiska Hugo‑webbplatsen underhölls och vidareutvecklades — hölls uppdaterad och fungerande — medan förbättringar av och återkoppling på UI/UX samtidigt fördes in i huvudprodukten för tillgångshantering. Att dela uppmärksamheten mellan en äldre webbplats och flaggskeppsprodukten handlar mest om att inte låta det gamla förfalla medan man fokuserar på det nya; båda representerar företaget inför någon, så båda måste hållas i hyfsat skick.</p>\n<p><strong>Resultat.</strong> Webbplatsen höll sig uppdaterad i stället för att i tysthet åldras, och huvudproduktens användbarhet förbättrades genom stadig, välgrundad förfining — den sorten som kommer av att faktiskt använda och fundera över produkten i stället för att göra om den i ett enda stort svep.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Frontendutveckling", "Fullstackutveckling", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/designed-a-time-management-and-reporting-system-that-88/", "url": "https://engineer.company/sv/portfolio/designed-a-time-management-and-reporting-system-that-88/", "title": "Utformade ett system för tidshantering och rapportering som användes i produktion i flera år utan större ändringar.", "summary": "Utformade ett system för tidshantering och rapportering som var i produktion i flera år utan större ändringar.", "content_html": "<p><strong>Situation.</strong> Teamet saknade ett pålitligt sätt att hantera tid och rapportera framsteg. Det brukar betyda att det sker i en spridd samling kalkylblad och i folks minne, och att rapporteringen blir en rusch i slutet av varje period i stället för något som helt enkelt följer av hur folk arbetar.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att bygga ett system för båda delarna som faktiskt skulle hålla.</p>\n<p><strong>Handling.</strong> Ett system för tidshantering och rapportering utformades kring hur teamet faktiskt arbetade, i stället för att påtvinga en färdig standardprocess som de ändå skulle ha kringgått. Det är hela knepet med interna verktyg — passar verktyget det verkliga arbetsflödet använder folk det; motarbetar det arbetsflödet överger de det i tysthet och man är tillbaka i kalkylbladen. Därför byggdes det för att matcha verkligheten snarare än ett ideal.</p>\n<p><strong>Resultat.</strong> Systemet var i produktionsdrift i flera år utan några betydande ändringar. Det är den komplimang man vill ha för ett internt verktyg — inte att det var imponerande, utan att det bara fortsatte att fungera och att ingen någonsin behövde ersätta det. Något som klarar flera år av daglig användning utan att röras var uppenbarligen byggt för att passa.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Produkt och krav", "Projektledning", "Tekniskt ledarskap", "Dataanalys och BI‑dashboards", "Produktstrategi och krav", "Projektledning (agil)" ] }, { "id": "https://engineer.company/sv/portfolio/automated-team-collaboration-password-management-task-and-time-89/", "url": "https://engineer.company/sv/portfolio/automated-team-collaboration-password-management-task-and-time-89/", "title": "Automatiserade teamets samarbete, lösenordshantering samt uppgifts- och tidshantering och byggde ett halvautomatiskt system för att visa upp projekt, vilket höjde teamets produktivitet.", "summary": "Automatiserade samarbete samt lösenords-, uppgifts- och tidshantering, byggde ett halvautomatiskt projektgalleri och höjde teamets produktivitet.", "content_html": "<p><strong>Situation.</strong> Mycket av teamets operativa arbete — samordning, lösenordshantering, uppföljning av uppgifter och tid — sköttes för hand, och manuell samordning är en tyst skatt: det är aldrig något man lägger märke till, men den äter stadigt upp timmar som kunde ha gjort bättre nytta någon annanstans.</p>\n<p><strong>Uppgift.</strong> Målet var att automatisera det repetitiva operativa arbetet.</p>\n<p><strong>Handling.</strong> De delar som lämpade sig för det automatiserades — teamsamarbete, lösenordshantering, uppgiftshantering, tidshantering — och ovanpå det ett halvautomatiskt system för att visa upp projekt. Tanken bakom allt detta var att lyfta den rutinmässiga samordningen från folks axlar så att den skötte sig själv, och låta dem lägga den återvunna uppmärksamheten på arbete som verkligen behövde en människa. Systemet för att visa upp projekt var samma instinkt tillämpad på något mer synligt: att göra presentationen av arbetet till något som mestadels sker automatiskt i stället för ett manuellt måste varje gång.</p>\n<p><strong>Resultat.</strong> Den operativa effektiviteten ökade och teamet blev mer produktivt, eftersom den rutinmässiga samordning som tidigare krävde ständig mänsklig uppmärksamhet nu i stort sett skötte sig själv. Tiden som tidigare läckte ut i administrativt pyssel gick tillbaka till det verkliga arbetet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Projektledning", "Säkerhet", "Tekniskt ledarskap", "DevOps och CI/CD‑automatisering", "Projektledning (agil)", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/integrated-a-company-wide-password-management-system-strengthening-90/", "url": "https://engineer.company/sv/portfolio/integrated-a-company-wide-password-management-system-strengthening-90/", "title": "Integrerade ett företagsövergripande system för lösenordshantering, vilket stärkte säkerheten och effektiviserade åtkomstkontrollen.", "summary": "Integrerade ett företagsövergripande system för lösenordshantering, stärkte säkerheten och effektiviserade åtkomstkontrollen i hela organisationen.", "content_html": "<p><strong>Situation.</strong> Inloggningsuppgifter hanterades inkonsekvent — olika personer lagrade och delade dem på olika, improviserade sätt — och just den inkonsekvensen är i sig säkerhetsrisken. Det handlar sällan om ett dramatiskt intrång; det är ett lösenord i ett chattmeddelande, en delad inloggning som ingen byter, den långsamma ansamlingen av små exponeringar.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att centralisera inloggningsuppgifterna och göra dem säkra.</p>\n<p><strong>Handling.</strong> Ett lösenordshanteringssystem för hela företaget infördes, så att det fanns ett enda konsekvent och säkert sätt att lagra och dela inloggningsuppgifter i stället för allas personliga vanor. Värdet i att det gäller hela företaget är just att det inte är frivilligt för den enskilda — en lösenordshanterare som bara halva teamet använder hjälper knappt, eftersom risken bor i den halva som låter bli. Poängen var alltså att göra det säkra sättet till standardsättet, överallt.</p>\n<p><strong>Resultat.</strong> Säkerheten förbättrades och åtkomstkontrollen blev enklare och mer konsekvent i hela organisationen. När alla inloggningsuppgifter finns på ett enda hanterat ställe blir en hel rad små, tråkiga exponeringar helt enkelt omöjliga — och det är det mesta av vad säkerhet i verkligheten faktiskt är.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Systemadministration", "Säkerhet", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/drove-client-web-success-by-combining-custom-website-91/", "url": "https://engineer.company/sv/portfolio/drove-client-web-success-by-combining-custom-website-91/", "title": "Drev kundernas framgång på webben genom att kombinera skräddarsydd webbutveckling och design med SEO, innehållsstrategi och copywriting.", "summary": "Drev fram kundernas framgång på webben genom att kombinera skräddarsydd webbutveckling och design med SEO, innehållsstrategi och copywriting.", "content_html": "<p><strong>Situation.</strong> Kunderna ville egentligen inte ha en webbplats; de ville ha det som en webbplats ska göra för dem — bli hittade, locka kunder, faktiskt fungera som en kanal. En vacker webbplats som ingen kan hitta är ett misslyckande som ser ut som en framgång.</p>\n<p><strong>Uppgift.</strong> Uppgiften var att slå ihop bygget och tillväxtsidan till ett enda erbjudande, i stället för att lämna över en webbplats och önska lycka till.</p>\n<p><strong>Handling.</strong> De två halvorna fördes samman — skräddarsydd webbutveckling och design på den ena sidan, SEO, innehållsstrategi och copywriting på den andra — så att kunden fick något som både var välbyggt och faktiskt gick att hitta. De två brukar behandlas som separata jobb, och det är så man till slut sitter med en praktfull webbplats som inte syns någonstans i sökresultaten, eller en väloptimerad webbplats som är otrevlig att använda. Att göra båda innebar att webbplatsen var utformad för att hittas redan från början, inte optimerad i efterhand.</p>\n<p><strong>Resultat.</strong> Kunderna fick bättre synlighet och mer engagemang — deras webbplatser fungerade som verkliga tillväxtkanaler i stället för som broschyrer på nätet. Att bygga webbplatsen och göra den sökbar i ett och samma svep var det som förvandlade den från en kostnad till något som faktiskt gjorde rätt för sig.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Frontendutveckling", "Produkt och krav", "UX/UI‑design", "Varumärke och marknadsföring", "Webbutveckling", "UI/UX‑design och designsystem", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/performed-data-recovery-across-a-wide-range-of-92/", "url": "https://engineer.company/sv/portfolio/performed-data-recovery-across-a-wide-range-of-92/", "title": "Utförde dataräddning från en lång rad medier — SD‑kort, HDD:er, SSD:er, RAID‑arrayer, externa diskar och Mac‑system.", "summary": "Räddade kritiska data från SD-kort, hårddiskar, SSD:er, RAID-system, externa diskar och Mac-system — filer tillbaka från enheter som avskrivits som förlorade.", "content_html": "<p><strong>Situation.</strong> Folk kom in med lagringsmedier som hade gått sönder eller skadats, med data på som de akut behövde tillbaka — och när någon bär in en död disk i en butik har oron oftast hunnit övergå i panik. Foton, affärsdokument, den enda kopian av något som betydde något.</p>\n<p><strong>Uppgift.</strong> Uppdraget var att rädda deras data, oavsett vilket medium de kom in med.</p>\n<p><strong>Handling.</strong> Dataräddningen omfattade i stort sett alla vanliga lagringstyper — SD‑kort, snurrande HDD:er, SSD:er, RAID‑arrayer, externa diskar, Mac‑system. Var och en av dem går sönder och lämnar ifrån sig sina data på sitt eget sätt: en SSD är ett annat problem än en disk med roterande skivor, en RAID‑array är ännu ett annat problem, och ett Mac‑filsystem har sina egna egenheter. Arbetet handlade alltså lika mycket om att veta vilket angreppssätt som passar just det mediet som om någon enskild teknik.</p>\n<p><strong>Resultat.</strong> Kunderna fick tillbaka kritiska data från enheter som de redan hade avskrivit som förlorade, på alla vanliga typer av lagring. Det finns en särskild lättnad i någons ansikte när man lämnar tillbaka disken med filerna i behåll — det var poängen med arbetet, och det gällde hela bredden av medier.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Hårdvarureparation och dataräddning", "IT‑support och helpdesk" ] }, { "id": "https://engineer.company/sv/portfolio/diagnosed-and-repaired-laptop-hardware-screens-hinges-keyboards-93/", "url": "https://engineer.company/sv/portfolio/diagnosed-and-repaired-laptop-hardware-screens-hinges-keyboards-93/", "title": "Diagnostiserade och reparerade hårdvara i bärbara datorer — skärmar, gångjärn, tangentbord, styrplattor, moderkort och strömförsörjning — och löste mjukvaruproblem i Linux, Windows och Mac.", "summary": "Felsökte och reparerade laptophårdvara — skärmar, gångjärn, tangentbord, styrplattor, moderkort, ström — och åtgärdade mjukvara på Linux, Windows och Mac.", "content_html": "<p><strong>Situation.</strong> Bärbara datorer kom in med hela registret av fel som drabbar bärbara datorer — spruckna skärmar, trasiga gångjärn, döda tangentbord, krånglande styrplattor, moderkortsfel, strömproblem — plus mjukvarusidan, i Linux, Windows och Mac. Kort sagt: vad som än var fel hamnade det på arbetsbänken.</p>\n<p><strong>Uppgift.</strong> Uppdraget var att diagnostisera och laga dem tillförlitligt — och tillförlitligt är nyckelordet, eftersom en datorreparation som inte håller bara är ett uppskjutet återbesök.</p>\n<p><strong>Handling.</strong> Hårdvaran reparerades från början till slut — skärmar, gångjärn, tangentbord, styrplattor, moderkort, strömförsörjning — vilket på moderkortsnivå är verkligt pilligt precisionsarbete, och mjukvaruproblemen löstes i alla tre operativsystemen. Diagnosen är oftast den verkliga konsten: en bärbar dator som inte startar kan bero på laddaren, kortet eller batteriet, och reparationen blir aldrig bättre än gissningen om vad som faktiskt är trasigt. Därför lades omsorgen på att hitta det verkliga felet innan något rördes.</p>\n<p><strong>Resultat.</strong> Datorerna gick tillbaka till sina ägare fungerande och pålitliga, med både hårdvara och mjukvara åtgärdade. En reparation som håller är den enda sortens reparation som är värd att göra, och att få diagnosen rätt först var det som fick just de här att hålla.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Linux och servrar", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Hårdvarureparation och dataräddning", "IT‑support och helpdesk" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/", "url": "https://engineer.company/sv/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/", "title": "Byggde en skiktad automatiserad testsvit — 981 Go‑tester, 543 specifikationer för frontend och webbläsare, 494 beteendetester i SQL — med mutationstestning, egenskapsbaserade tester och en tillgänglighetsgrind.", "summary": "Byggde en skiktad automatiserad testsvit — 981 Go-tester, 543 frontendspecar och 494 SQL-beteendetester — med mutations-, egenskaps- och tillgänglighetsgrindar.", "content_html": "<p><strong>Situation.</strong> En plattform som har sin affärslogik i databasen har ett testproblem som de flesta projekt slipper. Logiken finns inte i det språk som testramverket är bra på — den finns i SQL, bakom funktionsgränser, och SQL är precis den sortens kod som förblir otestad eftersom den är besvärlig att testa. Lägg till ett Go‑API och en Next.js‑frontend ovanpå det, så blir ”de viktiga delarna är täckta” i tysthet till ”de delar som var lätta att täcka är täckta”.</p>\n<p><strong>Uppgift.</strong> Varje lager behövde få sitt beteende verifierat där beteendet faktiskt finns, i stället för att allt verifierades utifrån genom en webbläsare.</p>\n<p><strong>Handling.</strong> Fyra lager fick fyra sorters tester. Go‑API:t har 981 testfunktioner fördelade på 310 filer. Frontenden har 543 testspecifikationer i Vitest och Playwright, däribland 48 end‑to‑end‑filer och 30 webbläsarspecifikationer. Databasen har 494 beteendetestfiler — 116 876 rader SQL som verifierar genom 6 654 kastade undantag — så att en lagrad funktion testas i databasen i stället för genom tre lager applikation ovanför. Ovanpå dem ligger testerna som testar testerna: mutationstestning med Stryker förstör medvetet en rad och fallerar när ingenting märker det, och fast‑check genererar indata som ingen tänkte på att skriva ned. En axe‑core‑grind kräver noll överträdelser av WCAG 2.0 och 2.1 på nivå A och AA, vilket gör tillgänglighet till ett byggfel i stället för ett granskningsfynd månader senare. Tröskelvärdena för testtäckning flyttas bara uppåt. Hela sviten körs som 10 jobb i ett arbetsflöde på 612 rader.</p>\n<p><strong>Resultat.</strong> Att ändra något strukturellt slutade vara skrämmande, och det är det enda som hindrar en kodbas av den här storleken från att förkalkas. Den ärliga kostnaden är tid — sviten är långsam, den belastar varje ändring, och i den här storleken behöver den eget underhåll. Det man får för den är förmågan att fortsätta i högt tempo, och det är värt mer än de minuter den tar.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Backendutveckling", "DevOps", "Frontendutveckling", "PostgreSQL", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/", "url": "https://engineer.company/sv/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/", "title": "Byggde repots kontrollmotor — 268 registrerade commit‑kontroller, 277 lintregler och 15 egna ESLint‑regler — plus 146 tester av själva kontrollerna, så att det är bygget som håller standarden, inte granskningen.", "summary": "Byggde en kontrollmotor med 268 registrerade commit-kontroller, 277 lintregler och 15 egna ESLint-regler, med 146 tester av själva kontrollerna.", "content_html": "<p><strong>Situation.</strong> Standarder som står nedskrivna i en bidragsguide är förslag. Alla håller med om dem, och sedan är det fredag, ändringen är liten och guiden förlorar. Kravet på noll varningar kunde bara hålla om något annat än god vilja höll uppe det — och de lintverktyg som följer med varje språk stannar långt före de projektspecifika regler som faktiskt spelar roll, de som handlar om hur just den här kodbasen är tänkt att fungera.</p>\n<p><strong>Uppgift.</strong> De regler som projektet brydde sig om måste gå att köra, så att ett brott mot en av dem stoppade committen i stället för att man fick vänta på en granskare med både tid och minne att fånga det.</p>\n<p><strong>Handling.</strong> Ur det växte en vaktmotor fram. Den har 274 kontrollskript, varav 268 är registrerade i commit‑hookarna, tillsammans med 277 lintverktyg för JavaScript samt 96 skalvaliderare och 17 Python‑validerare som täcker sådant som färdiga verktyg inte har någon åsikt om — att en migrering går att rulla tillbaka, att en översättningsnyckel finns i båda språkversionerna, att en registrerad route finns med i OpenAPI‑beskrivningen, att ingen i tysthet har lagt in en undertryckning av en varning direkt i koden. Femton egna ESLint‑regler upprätthåller projektets etablerade mönster i TypeScript. En regel förtjänar att nämnas för sig: varje commit med prefixet fix: måste innehålla ett test som fallerar utan ändringen, så att en bugg som är rättad förblir rättad. Och eftersom en trasig vakt är värre än ingen vakt alls — den släpper igenom allt och ingen märker det — har vakterna 146 egna tester.</p>\n<p><strong>Resultat.</strong> Tiden i kodgranskningen flyttades från mekanik till design, eftersom de mekaniska invändningarna redan hade gjorts av en maskin innan grenen pushades. Avvägningen är verklig och värd att säga rakt ut: det går långsamt att committa, och en dåligt skriven vakt är verkligen enerverande att ta sig runt. De 146 testerna finns för att det har hänt.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Tekniskt ledarskap", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-payments-and-entitlements-layer-96/", "url": "https://engineer.company/sv/portfolio/built-the-payments-and-entitlements-layer-96/", "title": "Byggde skiktet för betalningar och åtkomsträttigheter — Stripe vid sidan av köp i appen via Apple och Google — som spärrar katalogen, sökningen och exporten bakom en åtkomstmodell i 11 tabeller som kontrolleras på servern.", "summary": "Byggde lagret för betalningar och rättigheter — Stripe med köp i appen hos Apple och Google — som styr katalog, sökning och export utifrån en åtkomstmodell.", "content_html": "<p><strong>Situation.</strong> Pengar är den del av en plattform som ingen har råd att vara nonchalant med. En prenumeration måste klara ett kort som går ut, en återbetalning, ett planbyte, en webhook som kommer två gånger och en webhook som kommer i fel ordning. I samma stund som betalning kan ske i tre försäljningskanaler — ett kort på webben, Apple i den ena appbutiken, Google i den andra — finns det tre olika versioner av vad någon har köpt, och produkten behöver ändå ett enda svar på en enda fråga: vad får den här personen göra just nu?</p>\n<p><strong>Uppgift.</strong> Att ta betalt och att ge behörighet måste vara två system och inte ett, så att en ny försäljningskanal inte skulle innebära att varje spärr i produkten måste skrivas om.</p>\n<p><strong>Handling.</strong> Stripe hanterar kort och prenumerationer genom 18 Go‑filer, och köp i apparna hos Apple och Google kommer in via sin egen kvittoverifiering. Alla tre strålar samman i ett betalningsschema med 9 tabeller och 34 funktioner — och stannar där. Det som produkten faktiskt ställer sina frågor till är ett separat åtkomstschema, 11 tabeller och 34 funktioner, som svarar på ”får det här kontot göra det här?” utan att veta eller bry sig om vilken försäljningskanal som betalade. Det svaret styr åtkomsten till företagskatalogen, sökningen och dataexporten, och det verifieras på nytt på servern vid varje anrop, eftersom en dold knapp är en artighet och inte ett skydd.</p>\n<p><strong>Resultat.</strong> Att lägga till en försäljningskanal berör nu betalningssidan och lämnar spärrarna orörda, och en supportfråga om någons åtkomst har en tabell att slå upp i stället för tre. Kostnaden är två scheman där en mindre produkt skulle nöja sig med ett, plus ett behörighetsuppslag på anrop som annars hade varit gratis.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Databaser", "PostgreSQL", "Produkt och krav", "Säkerhet", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Produktstrategi och krav", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/moved-slow-work-onto-a-river-job-queue-97/", "url": "https://engineer.company/sv/portfolio/moved-slow-work-onto-a-river-job-queue-97/", "title": "Flyttade långsamt arbete från anropsvägen till en jobbkö i River — 15 workermoduler, 8 schemalagda uppgifter och 20 pg_cron‑jobb — så att ett anrop returnerar medan arbetet bakom det fortsätter.", "summary": "Flyttade långsamt arbete från anropsvägen till en jobbkö i River: 15 workermoduler, 8 schemalagda uppgifter och 20 underhållsjobb i pg_cron.", "content_html": "<p><strong>Situation.</strong> Visst arbete har inget att göra medan en användare väntar. Att skicka e‑post, bygga om ett sökindex, generera ett dokument, räkna om placeringar — gör något av det inne i anropet, och användaren sitter och tittar på en snurra för något hen aldrig bad om att få se. Gör det i en goroutine i stället, och det försvinner i samma stund som processen startas om, vilket den kommer att göra, mitt under en driftsättning, utan några spår av att det någonsin var tänkt att ske.</p>\n<p><strong>Uppgift.</strong> Bakgrundsarbetet behövde ett beständigt ställe att bo på: en kö som överlever en omstart, försöker igen när något misslyckas och går att inspektera när något inte har hänt.</p>\n<p><strong>Handling.</strong> Valet föll på River, till stor del eftersom den håller sin kö i PostgreSQL — databasen är redan den auktoritativa källan, så ett jobb och raderna det berör committas eller rullas tillbaka tillsammans, och det finns ingen andra infrastrukturdel att driva och resonera kring. Bakom den finns 15 worker‑moduler och 8 schemalagda uppgifter. Därunder sköter 20 pg_cron‑jobb det underhåll som databasen själv är bättre lämpad att göra: att rensa partitioner, rotera salt, uppdatera aggregat. Allt som var tillräckligt långsamt för att märkas flyttades bort från anropsvägen och över till någon av de två.</p>\n<p><strong>Resultat.</strong> Anrop returnerar snabbt och det långsamma arbetet blir ändå klart, med nya försök och en synlig historik när det inte blir det. Att hålla kön i Postgres i stället för i en dedikerad broker är en medveten begränsning: den kommer inte att skala i all evighet, och vid en viss volym blir den fel svar. För en plattform vars flaskhals ändå är databasen var en rörlig del mindre värd mer än marginal som ändå inte skulle komma till användning.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Backendutveckling", "Databaser", "PostgreSQL", "Prestandaoptimering", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/built-first-party-error-monitoring-and-tracing-98/", "url": "https://engineer.company/sv/portfolio/built-first-party-error-monitoring-and-tracing-98/", "title": "Byggde egen felövervakning och OpenTelemetry‑spårning i stället för att köpa dem — sanering av nyttolaster, upptäckt av toppar och regressioner, symbolisering och ett syntetiskt hjärtslag — bakom 11 operatörsvyer.", "summary": "Byggde egen felövervakning och OpenTelemetry-spårning — sanering, spikdetektering, symbolisering och heartbeat — bakom 11 operatörsvyer.", "content_html": "<p><strong>Situation.</strong> Skillnaden mellan en plattform som är uppe och en plattform som fungerar är om någon alls skulle få veta det. Ett fel som en användare stöter på klockan elva på kvällen, på en sida som ingen testar, är osynligt om inte något går och samlar in det. Det vanliga svaret är att köpa en hostad felspårningstjänst, vilket är ett bra svar — och det innebär också att plattformens egna fel, stackspår och användarkontext går vidare till en tredje part.</p>\n<p><strong>Uppgift.</strong> Fel och spårdata måste samlas in, grupperas och göras möjliga att åtgärda, utan att plattformens inre lämnade plattformen.</p>\n<p><strong>Handling.</strong> Två delar byggdes. OpenTelemetry sköter spårningen över OTLP, så att ett långsamt anrop kan följas genom frontenden, API:t och databasen i stället för att gissas fram. Bredvid den finns en egenutvecklad felpipeline — en errmon‑tjänst och en ingest‑tjänst — som sanerar nyttolaster före lagring, grupperar fel till återkommande problem i stället för en platt lista, upptäcker toppar och regressioner med en karenstid så att en dålig driftsättning inte larmar någon fyrtio gånger, översätter minifierade stackspår från frontenden tillbaka till läsbar kod och kör ett syntetiskt hjärtslag för att bevisa att pipelinen själv lever. Allt hamnar i databasen som felhändelser, felgrupper, en inkorg, API‑latens och stackprov, och det presenteras i 11 operatörsvyer, däribland en för servicenivåmål.</p>\n<p><strong>Resultat.</strong> Fel blir en kö som någon kan beta av, och en regression gör sig till känna i stället för att upptäckas av en användare. Att bygga i stället för att köpa kostade verklig tid och innebär att det är ännu en sak att underhålla — en köpt felspårare hade varit igång samma eftermiddag. Det man fick i gengäld var att inget känsligt lämnar plattformen, och att larmreglerna passar den här plattformen i stället för en generisk.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "DevOps", "Infrastruktur", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/kept-the-schema-honest-across-1022-migrations-99/", "url": "https://engineer.company/sv/portfolio/kept-the-schema-honest-across-1022-migrations-99/", "title": "Höll schemat korrekt genom 1 022 migreringar med en CI‑grind som bygger databasen på båda sätten — en ny installation, och en installation plus varje migrering — och fäller när de två skiljer sig åt.", "summary": "Höll schemat korrekt genom 1 022 migreringar med en CI-grind som bygger databasen på båda sätten och misslyckas när de två skiljer sig åt.", "content_html": "<p><strong>Situation.</strong> Ett schema beskrivs två gånger i de flesta projekt: en gång av den initiala uppsättningen som bygger det från grunden, och en gång av de ackumulerade migreringar som fick det att växa. Båda ska ge samma databas. Ingenting verifierar att de gör det, så de glider isär — och glidningen syns inte förrän en ny miljö beter sig annorlunda än produktionen, oftast i sämsta tänkbara ögonblick.</p>\n<p><strong>Uppgift.</strong> De två beskrivningarna måste bevisligen vara identiska, automatiskt, i stället för att man då och då tror att de är det.</p>\n<p><strong>Handling.</strong> Databasen är versionshanterad som 1 022 migreringar, numrerade från 036 till 1102, och ordningsdisciplinen kring dem är oglamorös och inte förhandlingsbar. Det som får det att hålla är ett CI‑jobb som bygger databasen två gånger vid varje ändring: en gång från det färska initiala schemat, en gång från det initiala schemat plus samtliga migreringar körda i ordning — och sedan jämför de två. Inte bara strukturen, som är den enkla halvan, utan även grunddata, eftersom en migrering som fyller i en uppslagstabell i efterhand på fel sätt är precis lika skadlig som en som glömmer en kolumn, och bara en av dem syns i en schemadiff. Varje avvikelse får bygget att fallera, med skillnaden utskriven.</p>\n<p><strong>Resultat.</strong> En ny miljö och en långlivad miljö är samma databas, och det verifieras i stället för att antas. Kostnaden hamnar hos den som skriver en migrering: den måste fungera när den spelas upp i ordning och den måste fungera från noll, vilket kräver mer eftertanke än en snabb ALTER brukar få. Det är just poängen — alternativet är att upptäcka det under en återställning, när svaret spelar roll och det inte finns tid att lista ut det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Databaser", "Datastyrning", "DevOps", "Migrering och modernisering", "PostgreSQL", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Databasadministration (DBA)", "Databasmigrering och modernisering", "DevOps och CI/CD‑automatisering" ] }, { "id": "https://engineer.company/sv/portfolio/built-fail-closed-abuse-controls-and-rate-limiting-100/", "url": "https://engineer.company/sv/portfolio/built-fail-closed-abuse-controls-and-rate-limiting-100/", "title": "Byggde skydd mot missbruk som stänger vid fel — 22 hastighetsbegränsare i Redis, Cloudflare Turnstile, idempotenta anrop och ett ursprungslås — så att plattformen avvisar botar och anropsfloder i stället för att lita på sina anropare.", "summary": "Byggde skydd mot missbruk som stänger vid fel: 22 Redis-baserade hastighetsbegränsare, Cloudflare Turnstile, idempotenta anrop och ursprungslås vid ingången.", "content_html": "<p><strong>Situation.</strong> En publik katalog över företag och yrkespersoner är en måltavla från den dag den lanseras. Skrapbotar vill åt datan, spamkonton vill åt räckvidden, och en endpoint som kostar plattformen riktiga pengar att leverera — sökning, export, allt som berör ett externt API — är värd att missbruka helt enkelt för att den är gratis att anropa. Inget av det är illvilja riktad mot just den här plattformen; det är en del av vädret på det öppna internet.</p>\n<p><strong>Uppgift.</strong> De kostsamma anropsvägar som går att missbruka behövde gränser som håller under tryck, inklusive det tryck som uppstår när anropsbegränsarens eget beroende inte är tillgängligt.</p>\n<p><strong>Handling.</strong> Det finns 22 anropsbegränsare, var och en konstruerad för den väg den skyddar i stället för ett enda globalt tak, eftersom ett inloggningsförsök, en sökning och en massexport blir missbruk vid helt olika frekvenser. Tillståndet ligger i Redis, så att en gräns delas mellan instanser i stället för att gälla per process och vara trivial att komma runt. Det viktiga beslutet är vad som händer när Redis inte finns där: anropsbegränsarna stänger vid fel. Trafiken nekas i stället för att vinkas igenom, vilket är det mindre bekväma svaret och det enda försvarbara. Runt dem finns Cloudflare Turnstile på de vägar som är värda att utmana, 351 rader middleware för idempotens så att en skrivning som görs om inte blir två, ett ursprungslås som nekar anrop som inte kommer in genom ytterdörren, och en specialbyggd utmaning på själva katalogen.</p>\n<p><strong>Resultat.</strong> Missbruk blir dyrt för den som missbrukar och billigt för plattformen, och ett avbrott i anropsbegränsarens egen lagring övergår i nekade anrop i stället för i en öppen dörr. Att stänga vid fel innebär visserligen att ett Redis‑problem blir ett problem som användarna ser — vilket accepterades medvetet, eftersom alternativet är att ett Redis‑problem blir ett faktureringsproblem.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Backendutveckling", "Nätverk och VPN", "Prestandaoptimering", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "Säkerhet och behörighetshantering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/built-first-party-product-analytics-in-postgresql-101/", "url": "https://engineer.company/sv/portfolio/built-first-party-product-analytics-in-postgresql-101/", "title": "Byggde egen produktanalys i PostgreSQL — 47 funktioner över partitionerade händelsetabeller som rensar sig själva — pseudonymiserad bakom ett roterande salt och villkorad av besökarens samtycke.", "summary": "Byggde egen produktanalys i PostgreSQL — 47 funktioner över självrensande partitionerade tabeller — pseudonymiserad och villkorad av samtycke.", "content_html": "<p><strong>Situation.</strong> Produktbeslut behöver siffror, och det vanliga sättet att få dem är att sätta en tagg från tredje part på varje sida och låta någon annans servrar iaktta användarna. Det går snabbt, det är gratis vid små volymer, och det innebär att besökarnas beteende på en professionell nätverksplattform — vem som tittade på vilket företag, vem som sökte efter vad — blir en tillgång som ägs av ett reklamföretag. För en plattform vars användare är identifierbara yrkesverksamma inom sjöfarten är det en dålig affär.</p>\n<p><strong>Uppgift.</strong> Produktteamet behövde trattar, retention och händelsedata, insamlade på ett sätt som plattformen kunde stå för och som en användare kunde tacka nej till.</p>\n<p><strong>Handling.</strong> Analysen byggdes in i PostgreSQL, i ett schema med 7 tabeller och 47 funktioner. Händelsetabellerna är partitionerade — 22 partitionsdefinitioner — och rensar sig själva enligt en tidsplan, eftersom kostnaden för förstapartsanalys inte ligger i att samla in händelserna utan i att behålla dem för alltid. Identiteten pseudonymiseras bakom ett salt som roteras, så att en besökare inte kan följas över rotationsgränsen ens inifrån databasen; det är ett medvetet tak för vad datan kan besvara. Klienten tar hänsyn till samtycke och skickar ingenting innan samtycke har getts, i stället för att skicka och filtrera i efterhand. Konverteringstrattar beräknas i databasen, intill datan, i stället för att exporteras någon annanstans för att sedan kopplas ihop igen.</p>\n<p><strong>Resultat.</strong> Frågor om hur produkten används besvaras ur plattformens egna tabeller, utan att något lämnar plattformen och utan någon tagg på sidan. Begränsningarna är den ärliga delen: att rotera saltet kostar kohortanalys över lång tid, och inget av detta kommer med de dashboards som ett hostat verktyg ger dig gratis. Båda accepterades medvetet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Data engineering", "Dataanalys", "Databaser", "Datastyrning", "PostgreSQL", "Prestandaoptimering", "SQL", "Dataanalys och BI‑dashboards", "Databasdesign och datamodellering", "Datastyrning och datakvalitet", "Utveckling av datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-operator-back-office-for-the-platform-102/", "url": "https://engineer.company/sv/portfolio/built-the-operator-back-office-for-the-platform-102/", "title": "Byggde operatörernas backoffice — ca 40 administrationsrutter och 128 komponenter för anspråk, moderering, funktionsflaggor, cache och diagnostik — så att plattformen kan drivas utan databasåtkomst.", "summary": "Byggde operatörernas back office — omkring 40 adminrutter och 128 komponenter för anspråk, moderering, funktionsflaggor, cache och diagnostik.", "content_html": "<p><strong>Situation.</strong> Varje plattform får i tysthet en andra applikation, och det är oftast den som ingen planerar. Någon måste godkänna ett anspråk, dölja en recension, slå på en funktion för en delmängd av användarna, rensa en cache eller ta reda på varför ett visst konto ser något konstigt. När den applikationen inte finns blir svaret en ingenjör med en databaskonsol — vilket är långsamt, ologgat och ett skrivfel från en incident.</p>\n<p><strong>Uppgift.</strong> Den dagliga driften av plattformen måste gå att sköta utan ett skal, så att driften tillhörde den som hade jouren och inte den som hade inloggningsuppgifterna.</p>\n<p><strong>Handling.</strong> Administrationsgränssnittet landade på ungefär 40 admin‑routes byggda av 128 komponenter, och det täcker arbetet så som det faktiskt kommer in: att pröva anspråk på företagsägande, moderera recensioner och inlägg, slå om funktionsflaggor, inspektera och rensa cacheminnen, läsa diagnostik, och den rapportering som vd:n ber om. Det körs på samma designsystem och samma genererade API‑klient som den publika produkten, vilket var det avgörande valet — ett internt verktyg byggt på en egen stack blir den del som ingen uppdaterar, och sedan den del som ingen litar på.</p>\n<p><strong>Resultat.</strong> De som driver plattformen kan driva den, och varje åtgärd går genom samma auktorisering och lämnar samma spår som allt annat. Det är en stor yta att hålla testad och tillgänglig för en liten intern publik, och den kostnaden är löpande. Det är ändå billigare än alternativet, nämligen en ingenjör som skriver UPDATE mot produktion klockan nio en söndagskväll.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Frontendutveckling", "Fullstackutveckling", "Produkt och krav", "Systemadministration", "UX/UI‑design", "Fullstackutveckling av produkter", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/built-company-ownership-claims-end-to-end-103/", "url": "https://engineer.company/sv/portfolio/built-company-ownership-claims-end-to-end-103/", "title": "Byggde anspråk på företagsägarskap från början till slut — en användare gör anspråk på ett företag, en administratör avgör, och ett godkännande skriver om auktoriseringsgrafen som bestämmer vem som får redigera vad.", "summary": "Byggde ägaranspråk på företag hela vägen: en användare gör anspråk, en administratör prövar det och ett godkännande skriver om behörighetsgrafen.", "content_html": "<p><strong>Situation.</strong> En katalog som har byggts upp från offentliga källor har ett strukturellt problem: företagen i den har inte lagt in sig själva. Förr eller senare dyker någon från ett av dem upp och vill rätta sin egen post — och det finns ingen koppling mellan den personen och den posten, bara ett påstående om att en sådan finns. Bevilja det för lättvindigt och en konkurrent redigerar din sida. Bevilja det för långsamt och katalogen förblir fel.</p>\n<p><strong>Uppgift.</strong> Det måste finnas en väg från ”det här är mitt företag” till verklig bestämmanderätt över posten, med ett mänskligt beslut i mitten och ett spår bakom sig.</p>\n<p><strong>Handling.</strong> Anspråksflödet byggdes från början till slut, över 108 commits och båda applikationerna. En användare skickar in ett anspråk med underlag; det hamnar i en kö i administrationsgränssnittet; en administratör granskar det och godkänner eller avslår det med en motivering som går tillbaka till den som gjort anspråket. Den intressanta delen är vad ett godkännande gör — det är inte en flagga på en rad. Godkännandet skriver om auktoriseringsgrafen, så att kontot får en verklig relation till organisationen, och det är samma relation som varje behörighetskontroll i plattformen redan utgår från. Spärren är prövningen, inte kodvägen, och ingen funktion behövde lära sig något om anspråk för att respektera den.</p>\n<p><strong>Resultat.</strong> Företag kan ta över och rätta sina egna poster utan att någon redigerar databasen för hand, och varje beviljad behörighet har en namngiven godkännare och en motivering kopplad till sig. Mänsklig granskning är avsiktligt flaskhalsen; en automatisk kontroll av ett domännamn skulle vara snabbare och skulle ha fel i exakt de fall som betyder mest.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Frontendutveckling", "Fullstackutveckling", "Produkt och krav", "Säkerhet", "Fullstackutveckling av produkter", "Produktstrategi och krav", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/established-a-continuous-security-programme-104/", "url": "https://engineer.company/sv/portfolio/established-a-continuous-security-programme-104/", "title": "Etablerade ett kontinuerligt säkerhetsprogram — kodskanning, DAST, kontroller av beroenden och sårbarheter, generering av SBOM, skanning efter hemligheter och SHA‑låsta actions — vid sidan av 21 skriftliga säkerhetsgranskningar.", "summary": "Etablerade ett kontinuerligt säkerhetsprogram — kodskanning, DAST, beroendekontroller, SBOM, hemlighetsskanning och låsta actions — plus 21 granskningar.", "content_html": "<p><strong>Situation.</strong> Säkerhetsarbetet i applikationen — innehållssäkerhetspolicyn, databasrollerna med minsta möjliga behörighet, de upprepade behörighetskontrollerna — skyddar plattformen under körning. Inget av det säger något om det som levereras: om ett beroende fick en känd sårbarhet i tisdags, om en inloggningsuppgift committades och sedan togs bort igen, eller om en action från tredje part som är låst till en tagg i tysthet har blivit en helt annan kodbit.</p>\n<p><strong>Uppgift.</strong> Säkerheten i leveranskedjan och i koden måste vara kontinuerlig och automatiserad, så att dess tillstånd var ett byggresultat och inte en åsikt.</p>\n<p><strong>Handling.</strong> Statisk analys körs med CodeQL, dynamisk testning mot en körande instans med ZAP, och Go‑beroenden skannas med govulncheck. En materialförteckning för programvaran genereras vid varje bygge med Anchore och Syft, så att det som levererades är känt i stället för rekonstruerat i efterhand. Gitleaks söker igenom historiken efter inloggningsuppgifter. Varje GitHub Action från tredje part är låst till en commit‑hash i stället för en tagg, vilket är den oglamorösa skyddsåtgärd som hindrar att någon flyttar en tagg bakom ryggen på en. Dependabot bevakar 6 ekosystem. Vid sidan av automatiseringen finns 21 skriftliga säkerhetsgranskningar, däribland en hotmodell och en bedömning mot OWASP:s testguide — eftersom skannrar hittar de kategorier av problem som någon redan har beskrivit, och en hotmodell är där de problem som är specifika för den här plattformen får ett namn.</p>\n<p><strong>Resultat.</strong> En sårbarhet som offentliggörs uppströms visar sig som ett bygge som fallerar i stället för som en nyhet. Mängden fynd är den verkliga kostnaden — en skanner som rapporterar allt lär folk att ignorera den, och att hålla signalen användbar kräver löpande triagering i stället för en engångsinstallation.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Säkerhet och behörighetshantering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-loyalty-and-reputation-system-105/", "url": "https://engineer.company/sv/portfolio/built-the-loyalty-and-reputation-system-105/", "title": "Byggde lojalitets- och ryktessystemet — 67 funktioner över en huvudbok i 31 tabeller, med ligor, märken och en inlösenbutik — som tar ett radlås på saldot för att stänga fönstret för dubbelspendering.", "summary": "Byggde lojalitets- och anseendesystemet — 67 funktioner över en huvudbok med 31 tabeller, ligor, märken och en butik — med radlåsning på saldot.", "content_html": "<p><strong>Situation.</strong> Professionella nätverk har ett kallstartsproblem: plattformen är värd att använda när andra redan använder den, och fram till dess finns det inte mycket som lockar tillbaka. Den vanliga hävstången är ett belöningssystem — poäng för att bidra, en status som speglar anseende — vilket är lätt att beskriva och förrädiskt att bygga, för i samma stund som poäng kan spenderas är de pengar, och varje misstag som går att göra med pengar går att göra här också.</p>\n<p><strong>Uppgift.</strong> Bidrag måste gå att mäta och belöna, med ett saldo som inte kan spenderas två gånger.</p>\n<p><strong>Handling.</strong> Lojalitetsschemat omfattar 31 tabeller och 67 funktioner, med status i ytterligare 5 tabeller och 32 funktioner, och bredvid dem finns scheman för upptäckt och personalisering som avgör vad en viss medlem ser. Ovanpå huvudboken finns ligor, märken och en inlösenbutik där ett saldo blir till något verkligt. Den del som krävde omsorg är den äldsta buggen av alla: kontrollera saldot, spendera det sedan, och två anrop som kommer samtidigt klarar båda kontrollen. Varje skrivoperation tar ett radlås på plånboken innan plånboken läses, så att det andra anropet väntar på att det första ska bli klart i stället för att kapplöpa med det. Att plånboken ligger i samma databas som allt annat är det som över huvud taget gör det möjligt — saldot och det som köptes för det committas tillsammans eller inte alls.</p>\n<p><strong>Resultat.</strong> Bidrag mäts och belönas, och ett saldo är aritmetik i stället för en uppskattning. Radlåsning är det långsammare svaret och valdes ändå: vid trängsel kring en plånbok får transaktionerna vänta på sin tur, och alternativet är en medlem som spenderar samma poäng två gånger och någon som får stämma av det för hand efteråt.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "PostgreSQL", "Prestandaoptimering", "Produkt och krav", "SQL", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-platform-s-social-layer-106/", "url": "https://engineer.company/sv/portfolio/built-the-platform-s-social-layer-106/", "title": "Byggde plattformens sociala skikt — inlägg, flöde, grupper, omnämnanden, en följargraf och en sammanställning av bemärkelsedagar — på samma funktionsbaserade dataskikt som resten av produkten.", "summary": "Byggde plattformens sociala lager — inlägg, flöde, grupper, omnämnanden, följargraf och en översikt över bemärkelsedagar — på samma funktionsbaserade datalager.", "content_html": "<p><strong>Situation.</strong> En katalog över företag är ett uppslagsverk. Folk slår upp något i den och går. Det som gör en professionell plattform värd att återvända till är att andra människor finns där — vilket innebär inlägg, grupper och ett skäl att komma tillbaka som inte är ett mejl som uppmanar dig att göra det.</p>\n<p><strong>Uppgift.</strong> Ett socialt lager behövde läggas till utan att det blev ett andra system med egna regler, egna behörigheter och ett eget sätt att lagra saker.</p>\n<p><strong>Handling.</strong> Inlägg, ett flöde, grupper, omnämnanden, en följargraf och en sammanställning av märkesdagar kom på plats genom 188 commits, allt på samma funktionscentrerade datalager som resten av plattformen. Den begränsningen gjorde det mesta av jobbet: följargrafen är tabeller och funktioner som allt annat, ett omnämnande slås upp genom samma identitetsschema som katalogen använder, och ett inlägg ärver den modereringskö som redan byggts för recensioner. Ingenting här behövde en egen lagring eller en egen behörighetsmodell. Flödet är det enda stället som bjöd motstånd, eftersom det är ett genuint annorlunda problem att effektivt sätta ihop en personlig tidslinje än att hämta en rad, och det är där arbetet med personalisering och upptäckt gör sig förtjänt av sin plats.</p>\n<p><strong>Resultat.</strong> Plattformen har ett skäl att öppnas en dag när ingen behöver slå upp ett företag, och det kom på plats utan en parallell stack att underhålla. Huruvida ett socialt lager är rätt investering för en maritim katalog är en produktfråga snarare än en teknisk fråga — det finns här för att produkten efterfrågade det, och det är byggt på samma sätt som allt runt omkring.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "Frontendutveckling", "Fullstackutveckling", "Produkt och krav", "Webbutveckling", "Backend- och API‑utveckling", "Fullstackutveckling av produkter", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-hiring-marketplace-and-career-workspace-107/", "url": "https://engineer.company/sv/portfolio/built-the-hiring-marketplace-and-career-workspace-107/", "title": "Byggde rekryteringsmarknadsplatsen och karriärarbetsytan för sjöfolk — 151 lagrade funktioner i 48 tabeller — med lediga tjänster, ansökningar, certifikat, befordran i grad och verifierad sjötid.", "summary": "Byggde jobbmarknadsplatsen och karriärytan för sjöfolk — 151 lagrade funktioner i 48 tabeller — som matchar lediga tjänster mot dokumenterade kvalifikationer.", "content_html": "<p><strong>Situation.</strong> Det kommersiella argumentet för ett professionellt nätverk inom sjöfarten är rekrytering: företag behöver besättning och befäl, och sjöfolk behöver platser ombord. Båda halvorna fanns redan på plattformen, men i fel form — företagen fanns i katalogen, yrkespersonerna hade profiler, och det fanns ingenting som kopplade en ledig tjänst till den person som var kvalificerad att fylla den. Kvalifikationen är den svåra delen, eftersom den i den här branschen består av certifikat, befattningar och dokumenterad sjötid snarare än en yrkestitel.</p>\n<p><strong>Uppgift.</strong> Lediga tjänster, ansökningar och verifierbara behörighetsbevis för sjöfolk måste modelleras ordentligt i stället för som fritext i en profil.</p>\n<p><strong>Handling.</strong> Två domäner byggdes. Rekryteringssidan omfattar 32 tabeller och 118 funktioner för lediga tjänster, ansökningar, urval och arbetsgivarens vy över rekryteringsflödet. Karriärarbetsytan har 16 tabeller och 33 funktioner som rymmer certifikat, befattningsutveckling och sjötid, med uppladdning av dokument och en granskningskö så att ett behörighetsbevis granskas i stället för att bara påstås. Att modellera utvecklingen som en graf i stället för en lista är det som gör matchningen användbar — en befattning går att nå från en annan befattning med vissa certifikat och tillräckligt med registrerad tid till sjöss, och den strukturen är det som gör att en ledig tjänst kan matchas mot en karriär i stället för mot ett nyckelord. Karriärarbetsytan levereras bakom en funktionsflagga och har inte släppts fullt ut; rekryteringssidan är i drift.</p>\n<p><strong>Resultat.</strong> En ledig tjänst kan matchas mot dokumenterade kvalifikationer i stället för mot en egenbeskriven yrkestitel, och det är hela skillnaden mellan en platsbank och ett rekryteringsverktyg i den här branschen. Verifieringen är flaskhalsen, medvetet — en granskningskö skalar inte på det sätt som en automatisk kontroll skulle göra, och en automatisk kontroll skulle certifiera personer som inte borde certifieras.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Databaser", "Datastyrning", "Fullstackutveckling", "PostgreSQL", "Produkt och krav", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Fullstackutveckling av produkter", "Produktstrategi och krav" ] }, { "id": "https://engineer.company/sv/portfolio/built-the-companys-infrastructure-as-code-108/", "url": "https://engineer.company/sv/portfolio/built-the-companys-infrastructure-as-code-108/", "title": "Byggde företagets egen infrastruktur som 19 Ansible‑playbooks och 34 roller på 12 065 rader YAML, som konvergerar en värd i drift mot ett deklarerat tillstånd, där varje play är idempotent.", "summary": "IT-miljön går att återskapa från repot, och de delar av den som bara var sanna för att någon kom ihåg dem är nu kontroller som får en körning att fallera.", "content_html": "<p><strong>Situation.</strong> Engineer ApS driver sin egen IT‑miljö — en webbnärvaro, en git‑forge, en databas, säkerhetskopior, e‑posttransport och DNS — och det fanns ingen att lämna över driften till. Ett enpersonsföretag kan fallera på samma sätt som ett stort men saknar helt dess redundans, vilket gör det vanliga svaret, en person som minns hur servern sattes upp, till det minst tillgängliga alternativ som finns.</p>\n<p><strong>Uppgift.</strong> Hela IT‑miljön måste beskrivas i ett repo i stället för i någons huvud, och beskrivas i en form som konvergerar en verklig server i stället för att dokumentera en.</p>\n<p><strong>Handling.</strong> Ur det växte 19 playbooks och 34 roller fram, fördelade på 12 065 rader YAML. Formen betyder mer än storleken. Sammansättningen är data snarare än flaggor: en server ingår i en nivågrupp vars variabler anger vilka roller den kör, så att provisionering utan argument konvergerar varje server till dess deklarerade tillstånd. Idempotens är ett kontrakt snarare än en ambition — en konvergerad server rapporterar noll ändringar, och en play som inte kan säga det är inte färdig. Fyra kommandonamnrymder håller isär löftena: en grind som inte rör någon server, rapporter som läser en server och aldrig ändrar den, provisionering som ändrar en server så att den stämmer med repot, och verifiering som ändrar en server med avsikt och returnerar ett utlåtande.</p>\n<p><strong>Resultat.</strong> IT‑miljön går att återskapa från repot, och de delar av den som bara var sanna för att någon kom ihåg dem är nu kontroller som får en körning att fallera. Kostnaden är verklig: varje ändring tar längre tid att göra än att redigera en fil på servern skulle ta, och en konvergering som bara tillämpas till hälften är värre än en som vägrar, vilket är skälet till att en preflight‑play senare lades till framför den. Den avvägningen gjordes medvetet och den har hållit.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Infrastruktur", "Linux och servrar", "Plattformsarkitektur", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Infrastruktur som kod", "Molninfrastruktur och migrering" ] }, { "id": "https://engineer.company/sv/portfolio/ran-the-whole-company-on-one-512mb-host-109/", "url": "https://engineer.company/sv/portfolio/ran-the-whole-company-on-one-512mb-host-109/", "title": "Drev hela företaget på en enda värd med 512 MB och en kärna — en git‑forge, en webbserver för sju domäner, Tor, två servrar för alternativa protokoll, säkerhetskopiering och intrångsblockering — genom att behandla 464 MB användbart minne som den bindande arkitektoniska begränsningen.", "summary": "Hela företaget körs på en maskin som kostar mindre per månad än en lunch, och designen är bättre tack vare disciplinen, inte bara billigare.", "content_html": "<p><strong>Situation.</strong> Företagets produktionsserver är en molninstans med en kärna, 512 MB minne och 10 GB disk, och av minnet går ungefär 464 MB att använda. Allt som verksamheten driver publikt ligger på den: webbservern som terminerar TLS för sju domäner, git‑forgen, en onion‑tjänst för Tor, en Gemini‑server, en Gopher‑server, krypterade säkerhetskopior och spärrning av intrångsförsök. Den vanliga reaktionen på en sådan lista är att köpa en större maskin.</p>\n<p><strong>Uppgift.</strong> Begränsningen måste behandlas som en förutsättning för arkitekturen snarare än som ett problem att lägga pengar på, eftersom den ärliga frågan inte var om en större maskin skulle fungera utan om designen behövde en.</p>\n<p><strong>Handling.</strong> Minnet blev det argument som avgjorde besluten. Det finns ingen övervakningsagent, ingen pipeline för mätvärden och ingen dashboard — rapporteringen är pull‑baserad: sju kommandon som läser servern och renderar Markdown, som inte ändrar någonting och bara körs när någon ber om det. Processerna övervakas av systemd i stället för av en andra processhanterare ovanpå den, och containerlagret består av Quadlet‑enheter under samma systemd i stället för en daemon med en egen processhanterare. Plattformar med webbpanel valdes bort redan i designskedet av samma skäl. När frågan kom upp om servern klarade att bära en onion‑tjänst kom svaret från ett dygns mätningar snarare än från en åsikt: tillgängligt minne föll aldrig under ungefär 310 MB av 464, swap låg på 2,6 % och processorn var i vila 99,7 % av tiden.</p>\n<p><strong>Resultat.</strong> Hela företaget körs på en maskin som kostar mindre per månad än en lunch, och designen är bättre tack vare disciplinen, inte bara billigare. Priset är att det saknas marginal för slarv — e‑post ligger medvetet inte alls på den här maskinen — e‑postdelen är skriven som ett installationsskelett som väntar på en egen server, eftersom en e‑postserver behöver marginal som den här maskinen redan har förbrukat, och det står nedskrivet i stället för att upptäckas senare.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Lösningsarkitektur", "Plattformsarkitektur", "Prestandaoptimering", "Systemadministration", "Tillförlitlighet och säkerhetskopiering", "Infrastruktur som kod", "Molninfrastruktur och migrering", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/found-three-ssh-brute-force-protections-that-never-worked-110/", "url": "https://engineer.company/sv/portfolio/found-three-ssh-brute-force-protections-that-never-worked-110/", "title": "Hittade och åtgärdade tre skydd mot brute force‑attacker över SSH som aldrig hade fungerat: en jail som bevakade port 22 medan daemonen lyssnade på 1986, en hastighetsbegränsning som skuggades av en bredare regel ovanför den och en spärråtgärd vars binärfil aldrig gick att hitta, så att ingen spärr någonsin hade tillämpats.", "summary": "Tre skydd som aldrig en enda gång hade slagit till gör det nu, och den klass av defekter som de tillhör — ett skydd vars sätt att fallera är att det fortsätter…", "content_html": "<p><strong>Situation.</strong> En härdningsgranskning av produktionsservern i augusti 2026 ställde en fråga som brukar få ett självsäkert svar: fungerar skydden mot brute force‑attacker via SSH. Alla tre var konfigurerade, alla tre syntes i varje rapport som någon tittade på, och alla tre hade varit verkningslösa sedan den dag servern byggdes.</p>\n<p><strong>Uppgift.</strong> Skydden måste kontrolleras mot vad kärnan faktiskt gör med ett paket, i stället för mot de konfigurationsfiler som beskriver vad som borde hända med det.</p>\n<p><strong>Handling.</strong> Att läsa konfigurationen hade bekräftat fel svar tre gånger, så granskningen läste det körande systemet i stället. Jailen för spärrning av intrångsförsök bevakade port 22, medan daemonen hade flyttats till 1986 under den första härdningen — varje spärr den skrev angav en port som ingenting lyssnade på. Brandväggens hastighetsbegränsning var värre på ett subtilare sätt: regeln fanns, och den låg under en bredare regel som matchade först. Brandväggens användarregler utvärderas uppifrån och ned och den första matchningen vinner, så en bred tillåtande regel ovanför en hastighetsbegränsning gör begränsningen till död kod som ändå syns i varje statuslistning. Det tredje skyddet var det tystaste av dem: spärråtgärden anropar via skalet en binär för paketfiltrering som paketsystemet bara rekommenderar i stället för att kräva, så på en server som saknar den startar jailen, räknar och fattar beslut, och fallerar sedan i just det ögonblick den försöker spärra. Alla tre åtgärderna var små. Det som kom ut av det var inte åtgärderna utan två regler som nu styr repot: ett säkerhetsskydd får en kontroll i stället för en kommentar, och en brandvägg verifieras utifrån reglernas position snarare än utifrån att reglerna finns.</p>\n<p><strong>Resultat.</strong> Tre skydd som aldrig en enda gång hade slagit till gör det nu, och den klass av defekter som de tillhör — ett skydd vars sätt att fallera är att det fortsätter att rapportera att allt är i sin ordning — är den klass som plattformens kontroller nu är byggda för att fånga. Alla tre hade varit verkningslösa ända sedan servern sattes upp. Alla tre visade att allt var i sin ordning överallt där någon tittade, vilket är hela anledningen till att de fick bestå.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Systemadministration", "Säkerhet", "Övervakning och observerbarhet", "Nätverk och VPN‑uppsättning", "Säkerhet och behörighetshantering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/hardened-ssh-with-three-stage-validation-111/", "url": "https://engineer.company/sv/portfolio/hardened-ssh-with-three-stage-validation-111/", "title": "Härdade SSH till 24 kontrollerade direktiv med validering i tre steg — kandidatfilen, den sammansatta konfigurationen och sedan daemonens egen återläsning — efter att återläsningen hade avslöjat att den körande servern tyst åsidosatte två av de tjugofyra.", "summary": "Säkerhetsläget för SSH är nu daemonens svar snarare än repots påstående, och skillnaden är inte teoretisk — den var redan två inställningar bred när kontrollen…", "content_html": "<p><strong>Situation.</strong> SSH är den enda interaktiva vägen in på företagets värd, och dess konfiguration skrivs av en automatiseringsroll som i månader hade gått felfritt. En åtkomstrapport i september 2026 läste av de inställningar som daemonen själv faktiskt tillämpade och fann två som avvek från det rollen hade skrivit vid varje enskild konvergenskörning.</p>\n<p><strong>Uppgift.</strong> Härdningen måste bli något som daemonen bekräftar snarare än något som repot påstår, eftersom glappet mellan de två redan hade stått öppet i månader utan att någon märkt det.</p>\n<p><strong>Handling.</strong> Orsaken var konfigurationens ordningsföljd. Operativsystemet levereras med egna standardvärden som inte är bortkommenterade, ovanför det ställe där en drop‑in‑fil hamnar, och för de två inställningarna i fråga vinner den första förekomsten. Lösningen var ett filnamnsprefix som sorteras före leverantörens, en ändring i storleksordningen ett tecken och precis den sortens ändring som förblir trasig eftersom ingen kommer på tanken att titta. Det som byggdes runt den väger tyngre: tre steg av validering vid varje konvergenskörning. Kandidatfilen syntaxkontrolleras innan den installeras, så att en ogiltig konfiguration aldrig når värden. Den sammansatta konfigurationen kontrolleras efter installationen. Sedan läses daemonens egen utdata med de gällande värdena tillbaka och 24 direktiv kontrolleras mot den, så att en inställning som har skrivits men åsidosatts får körningen att misslyckas. Två direktiv utelämnades medvetet, båda eftersom daemonen inte längre implementerar dem och att skriva dem bara skulle se grundligt ut.</p>\n<p><strong>Resultat.</strong> Säkerhetsläget för SSH är nu daemonens svar snarare än repots påstående, och skillnaden är inte teoretisk — den var redan två inställningar bred när kontrollen skrevs. En konvergenskörning som inte kan se om en säkerhetsåtgärd fick effekt har inte verifierat den, och att greppa efter ett direktiv bevisar bara att det skrevs.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Systemadministration", "Säkerhet", "Test och kvalitetssäkring", "Infrastruktur som kod", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/proved-the-intrusion-banning-path-on-every-converge-112/", "url": "https://engineer.company/sv/portfolio/proved-the-intrusion-banning-path-on-every-converge-112/", "title": "Bevisade spärrkedjan mot intrång från början till slut vid varje härdningskörning genom att spärra en reserverad testadress, läsa den kärnregel som uppstod och häva spärren i ett städblock som alltid körs, så att en jail som slutar fungera fäller körningen i stället för att rapportera att allt är i ordning.", "summary": "Ett spärrflöde som slutar fungera får nu en konvergenskörning att misslyckas i stället för att fortsätta rapportera felfri status, och det var den enda…", "content_html": "<p><strong>Situation.</strong> Tjänsten som spärrar intrångsförsök hade redan ertappats med att lägga spärrar på en port som SSH‑daemonen inte lyssnade på. Att rätta porten stängde just det fallet. Det gjorde ingenting åt orsaken till att felet hade överlevt så länge, nämligen att ett spärrflöde saknar synligt felläge: tjänsten körs, jailen listas som aktiv, och ingenstans står det om en spärr någonsin når kärnan.</p>\n<p><strong>Uppgift.</strong> Spärrflödet måste provas skarpt vid varje konvergenskörning, mot den levande regeluppsättningen, i stället för att härledas ur att tjänsten är igång.</p>\n<p><strong>Handling.</strong> Konvergenskörningen spärrar nu en adress ur det block som standarderna reserverar för dokumentation, läser tillbaka den resulterande regeln i kärnans paketfilter och häver spärren i ett städblock som körs oavsett om kontrollen gick igenom eller misslyckades. Det reserverade intervallet är det bärande valet — testadressen tillhör ingen, så en kvarglömd spärr som överlever en misslyckad körning kan inte stänga ute ett verkligt nätverk. Återläsningen från kärnan är den andra halvan: tjänstens egen statusutdata skulle rapportera framgång för en spärr som inte gav upphov till någon regel, vilket är exakt det fel som testas. Vid sidan av detta är spärrbackenden fastlåst i stället för att lämnas åt tjänstens egen identifiering, och loggbackenden är inställd på automatisk identifiering eftersom operativsystemet inte levereras med någon traditionell autentiseringslogg och fel val misslyckas tyst genom att bevaka en fil som aldrig dyker upp.</p>\n<p><strong>Resultat.</strong> Ett spärrflöde som slutar fungera får nu en konvergenskörning att misslyckas i stället för att fortsätta rapportera felfri status, och det var den enda egenskap som spelade någon roll. Det kostar några sekunder vid varje körning, och varje gång skrivs och tas en brandväggsregel bort i produktion, vilket är ett verkligt ingrepp i ett levande system och godtogs med motiveringen att alternativet redan hade visat sig vara sämre.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Säkerhet", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Systemadministration", "Säkerhet och behörighetshantering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/verified-firewall-rules-by-position-113/", "url": "https://engineer.company/sv/portfolio/verified-firewall-rules-by-position-113/", "title": "Verifierade brandväggsregler efter position snarare än förekomst och läste den numrerade regellistan och den aktiva paketfilterkedjan, eftersom en regel som finns inte är detsamma som en regel som något paket når.", "summary": "Brandväggens säkerhetsläge kontrolleras nu så som ett paket upplever det. Det mest användbara utfallet var inte kontrollen utan det den avslöjade om…", "content_html": "<p><strong>Situation.</strong> En statuslistning från brandväggen visar en uppsättning regler. Ett paketfilter utvärderar en ordnad lista och stannar vid första träffen. Det är olika saker, och skillnaden är osynlig i varje verktyg som skriver ut en sammanfattning — och det är så värden kunde gå i ett år med en hastighetsbegränsning som inget paket någonsin nådde fram till, placerad under en bredare regel som matchade först.</p>\n<p><strong>Uppgift.</strong> Brandväggsverifieringen måste läsa position i stället för medlemskap, eftersom det redan hade visats att närvaro inte bevisar någonting.</p>\n<p><strong>Handling.</strong> Kontrollerna läser nu den numrerade regellistan och den levande filterkedjan och verifierar mot ordningen. Det finns exakt en regel per port och alltid med ett protokoll, eftersom en regel utan protokoll i tysthet vidgar attackytan. SSH‑porten har en hastighetsbegränsning och aldrig en allow‑regel bredvid sig, eftersom en allow‑regel ovanför en begränsning är just den skuggning som upptäcktes. Den deklarerade publika ytan är en kort lista med fem portar, som verifieras som helhet snarare än en och en, så att en port som dyker upp utan att vara deklarerad ger ett fel i stället för att bara noteras. Säkerhetsrapporten återger reglerna i matchningsordning med en varning högst upp i avsnittet som förklarar hur den ska läsas, eftersom nästa person som öppnar filen annars kommer att läsa den som en uppsättning.</p>\n<p><strong>Resultat.</strong> Brandväggens säkerhetsläge kontrolleras nu så som ett paket upplever det. Det mest användbara utfallet var inte kontrollen utan det den avslöjade om rapporteringen som föregick den: varje inblandat verktyg hade skrivit ut hastighetsbegränsningen i ett år, korrekt, och inget av dem hade fått den enda fråga som spelade någon roll.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Nätverk och VPN", "Säkerhet", "Test och kvalitetssäkring", "Nätverk och VPN‑uppsättning", "Systemadministration", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/built-encrypted-backups-with-a-monthly-restore-drill-114/", "url": "https://engineer.company/sv/portfolio/built-encrypted-backups-with-a-monthly-restore-drill-114/", "title": "Byggde krypterade säkerhetskopior utanför värden med restic, med gallring enligt lagringstid, en integritetskontroll och en månatlig automatiserad återläsningsövning, granskade sedan återställningsförmågan och skrev ner luckorna i stället för att låta dem upptäckas under en incident.", "summary": "Företaget kan förlora värden och få tillbaka sina data, och den meningen vilar på en återställning som kördes förra månaden snarare än på en säkerhetskopiering…", "content_html": "<p><strong>Situation.</strong> Allt som företaget har — git‑repona, databasen, webbplatsen — låg på en enda molninstans, och leverantörens ögonblicksbild av den var hela återställningsberedskapen. En ögonblicksbild hos leverantören är bra att ha och dålig att förlita sig på: den ligger i samma konto som maskinen den skyddar, den krypteras inte av någon här, och ingen hade någonsin återställt något från en sådan.</p>\n<p><strong>Uppgift.</strong> Säkerhetskopiorna måste vara krypterade, ligga utanför värden, gallras enligt en bevarandepolicy och — den del som brukar hoppas över — faktiskt användas för återställning, enligt ett schema, utan att någon person behöver komma ihåg att göra det.</p>\n<p><strong>Handling.</strong> Säkerhetskopieringen körs på en systemd‑timer: en databasdump där det finns en databas, därefter en krypterad, deduplicerad ögonblicksbild till lagring hos en annan leverantör via SFTP, sedan en gallring enligt bevarandepolicyn och sist en integritetskontroll. Ett dödmansgrepp pingas bara när körningen lyckas, och det är den distinktionen som gör det till ett larm i stället för en logg — en körning som misslyckas säger ingenting, och det är tystnaden som utlöser larmet. Separat körs en återställningsövning varje månad: den hämtar ut en känd fil ur repot och jämför den, så att det som kontrolleras är en återställning och inte en säkerhetskopiering. Lösenfrasen skrivs till en fil som enheterna läser i stället för att skickas via miljön, eftersom tjänstehanteraren tolkar escape‑tecken i miljövärden och en lösenfras med ett omvänt snedstreck i tysthet skulle ha blivit en annan än den som skapade repot. Efteråt granskades återställningsberedskapen och dokumenterades, och de återstående luckorna namngavs i dokumentet i stället för att lämnas åt att upptäckas under en incident.</p>\n<p><strong>Resultat.</strong> Företaget kan förlora värden och få tillbaka sina data, och den meningen vilar på en återställning som kördes förra månaden snarare än på en säkerhetskopiering som kördes i natt. Granskningens mest värdefulla resultat var listan över det som fortfarande inte täcks, och det är den del som en grön säkerhetskopieringsrapport av strukturella skäl aldrig kan berätta för någon.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Infrastruktur", "Linux och servrar", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Infrastruktur som kod", "Systemadministration", "Säkerhetskopiering och återställning", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/built-dead-mans-switch-monitoring-115/", "url": "https://engineer.company/sv/portfolio/built-dead-mans-switch-monitoring-115/", "title": "Byggde övervakning med dödmansgrepp som pingar bara så länge minne och disk är i gott skick, så att en försämrad värd larmar genom att tystna — och upptäckte sex variabelnamn som sa ”free” där kontrollen korrekt mätte ”available”, en storleksordning isär på en maskin med 464 MB.", "summary": "Värden har nu ett larm vars felläge är att det löser ut, och namngivningen som skulle ha förstört det har rättats.", "content_html": "<p><strong>Situation.</strong> En värd på 464 MB som kör en git‑forge, en databas och en webbserver har två realistiska sätt att dö: minnet tar slut, eller disken tar slut. Inget av dem förvarnar. Båda går att förutse helt och hållet några timmar i förväg om något håller utkik, och ingenting gjorde det.</p>\n<p><strong>Uppgift.</strong> Värden behövde ett larm som fungerar när värden inte gör det — vilket utesluter allt som måste skicka ett meddelande i felögonblicket.</p>\n<p><strong>Handling.</strong> Svaret är ett dödmansgrepp på en timer. Var femtonde minut, med slumpmässig förskjutning, mäter ett litet skript tillgängligt minne och ledigt diskutrymme och pingar en extern tjänst bara om båda ligger över sina tröskelvärden. Tystnaden är larmet. En maskin som har fått slut på minne, tappat nätverket eller slutat starta ger exakt samma signal som en som inte är frisk, vilket är rätt beteende och skälet till att den här formen valdes framför en agent som rapporterar en status. Det som mäts är tillgängligt minne, inte ledigt minne, och den distinktionen blev den mest lärorika delen av arbetet: skriptet hade läst rätt kolumn hela tiden medan sex variabelnamn runt omkring sa ”free”. På en frisk Linux‑maskin ligger det lediga minnet nära noll eftersom kärnan använder outnyttjat minne som cache, så en läsare som jämförde de namnen mot ett tröskelvärde på tio procent skulle se tolv megabyte ledigt på en maskin med 464 MB, dra slutsatsen att kontrollen var trasig och rätta den med en ändring på ett tecken, som förvandlar ett fungerande larm till ett som löser ut permanent och tystas inom en vecka.</p>\n<p><strong>Resultat.</strong> Värden har nu ett larm vars felläge är att det löser ut, och namngivningen som skulle ha förstört det har rättats. Dödmansgreppets verkliga begränsning står i dess egen dokumentation: det bevisar att maskinen är frisk, inte att webbplatsen svarar, och det är olika frågor.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Prestandaoptimering", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Infrastruktur som kod", "Systemadministration", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/made-ansible-check-mode-tell-the-truth-116/", "url": "https://engineer.company/sv/portfolio/made-ansible-check-mode-tell-the-truth-116/", "title": "Fick check mode att tala sanning i hela plattformen efter att ha hittat sex sonder som fattade beslut utifrån ett värde som värddatorn aldrig gav, eftersom command‑modulen i Ansible rapporterar framgång under --check medan den hoppar över kommandot helt.", "summary": "En torrkörning mäter nu antingen något eller säger att den inte gjorde det, och inget av de två är det tredje alternativ den tidigare hade.", "content_html": "<p><strong>Situation.</strong> En torrkörning mot produktion ska vara det säkra sättet att ta reda på vad en ändring kommer att göra. I september 2026 föll en torrkörning på en kontroll som helt enkelt hade fel om värden, och rådde operatören att sätta en flagga som skulle ha luckrat upp en säkerhetssandlåda. Torrkörningen hade inte läst av värden. Den hade läst ett värde som värden aldrig gav och dragit en slutsats av det.</p>\n<p><strong>Uppgift.</strong> Varje sond vars resultat ligger till grund för ett beslut måste göras ärlig i check mode, och de som inte gick att göra ärliga måste säga det högt i stället för att förbli tysta.</p>\n<p><strong>Handling.</strong> Orsaken är en egenskap hos verktyget som är dokumenterad och lätt att glömma: command‑modulen körs inte i check mode, och det den registrerar är inte ett tomt resultat — det är en framgång med tom utdata. Varje villkor som läser den registrerade variabeln fattar därför beslut utifrån ett värde som aldrig mättes, och beslutet går åt det håll som dess egen logik råkar peka. En genomsökning av alla registrerade sonder hittade sex stycken, som ljög på var sitt sätt: en rapporterade att tjänstehanteraren accepterar varje enhetsdirektiv utan att ha frågat tjänstehanteraren, en annan rapporterade att inget behövde göras på en värd där brandväggen var avstängd. Var och en fick en av två former. En sond som läser befintligt tillstånd som körningen inte har rört markeras för att köras även i check mode. En sond som inte kan köras hoppas över, och ett meddelande anger vad som specifikt inte verifierades — eftersom tystnad i en körlogg ser exakt ut som ett godkänt resultat.</p>\n<p><strong>Resultat.</strong> En torrkörning mäter nu antingen något eller säger att den inte gjorde det, och inget av de två är det tredje alternativ den tidigare hade. Den allmänna regeln fördes in i utvecklarguiden i samma ändring: check mode får inte ljuga, och en sond som inte kan se måste tillkännage sin blindhet i stället för att härleda ett utslag ur den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Linux och servrar", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Infrastruktur som kod", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/added-a-preflight-play-for-secrets-117/", "url": "https://engineer.company/sv/portfolio/added-a-preflight-play-for-secrets-117/", "title": "Lade till en play för förhandskontroll som kör samma kod som konvergeringen mot operatörens lokala hemligheter på ungefär en sekund, efter att en halvt tillämpad produktionskörning kraschade vid sin nionde uppgift med swapinställningarna redan skrivna till värden i drift.", "summary": "En hemlighet som inte går att slå upp kostar nu en vägran på två sekunder på en bärbar dator i stället för en delvis tillämpad ändring på en levande server.", "content_html": "<p><strong>Situation.</strong> De operativa hemligheterna hade nyss delats upp så att varje värd har sitt eget arkiv för säkerhetskopior, sin egen lösenfras och sin egen larmbrytare. Koden i repot var korrekt. Operatörens krypterade vault‑fil innehöll fortfarande det tidigare värdet för en enda värd, och ingenting kunde se avvikelsen, eftersom vault‑filen medvetet ligger lokalt på operatörens maskin och är osynlig för repots egen kvalitetsgrind.</p>\n<p><strong>Uppgift.</strong> Den sortens fel behövde ett ställe där det kunde misslyckas billigt, eftersom det just hade misslyckats dyrt.</p>\n<p><strong>Handling.</strong> Den första konvergenskörningen efter ändringen anslöt till den levande värden, körde åtta tasks och vägrade vid den nionde — korrekt, men inifrån en körning som redan hade skrivit swap‑inställningar till produktion. En halvt tillämpad konvergenskörning är ett sämre svar än en vägran, så en förhandskontroll skrevs som en egen play: den når ingen värd alls, körs lokalt, samlar inte in några fakta och kör samma två task‑filer som den riktiga konvergenskörningen använder, mot operatörens vault‑fil. Den svarar på ungefär en sekund, och provisioneringen kör den först. Det bärande beslutet är att den är samma kod och inte en andra implementation av samma regel, eftersom en kontroll som återger en regel i ett annat språk till slut säger emot den, och gör det i tysthet. När den skrevs kom en fälla i dagen som den nästan själv hade skapat: fakta som sätts under en play överlever den playen, så om förhandskontrollen och konvergenskörningen hade kedjats ihop i ett enda anrop skulle den riktiga rollen ha fått en lösenfras som förhandskontrollen redan hade slagit upp, och kontrollen hade godkänts på en vault‑fil som fortfarande var fel. Felet reproducerades innan skyddet lades in, i båda task‑filerna.</p>\n<p><strong>Resultat.</strong> En hemlighet som inte går att slå upp kostar nu en vägran på två sekunder på en bärbar dator i stället för en delvis tillämpad ändring på en levande server. Förhandskontrollen är medvetet inte taggad för att alltid köras och är medvetet inte serialiserad, och båda delarna är dokumenterade som beslut snarare än som standardval.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Säkerhet", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "DevOps och CI/CD‑automatisering", "Infrastruktur som kod", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/reconciled-a-dns-zone-declaratively-118/", "url": "https://engineer.company/sv/portfolio/reconciled-a-dns-zone-declaratively-118/", "title": "Stämde av en DNS‑zon med 20 poster deklarativt mot Cloudflare‑API:t med separata ingångar för granskning och BIND‑export, och stängde av CDN‑proxyn igen av integritetsskäl efter att ha byggt den.", "summary": "Zonen är versionshanterad, jämförbar och exporterbar, och det enda beslut som gick emot det självklara standardvalet är nedskrivet med sitt resonemang, så att…", "content_html": "<p><strong>Situation.</strong> Företagets DNS innehöll ungefär tjugo poster fördelade på en webbnärvaro, en underdomän för git, två personliga omdirigeringar, e‑postroutning via en leverantör av hostade e‑postlådor, en avsändardomän för transaktionsmejl och autentiseringsposterna för båda. Allt låg i en leverantörs webbkonsol, vilket innebär att det aktuella tillståndet var det som den som senast klickade hade lämnat efter sig.</p>\n<p><strong>Uppgift.</strong> Zonen måste bli en deklaration i repot, med ett sätt att jämföra den deklarationen med vad leverantören faktiskt levererar.</p>\n<p><strong>Handling.</strong> Zonen skrivs som data — aktiva poster, applikationsposter, poster för transportpolicy och en separat lista över poster som väntar på borttagning, vilket är det ärliga sättet att registrera en borttagning som ännu inte har skett. En play stämmer av den mot leverantörens API. Ytterligare två ingångar ligger bakom uttryckliga opt‑in‑taggar: en granskning som rapporterar skillnaden utan att ändra något, och en export som skriver ut zonen i det vanliga zonfilsformatet så att den kan läsas av något annat än det här repot. Det intressantaste beslutet var en omsvängning. Proxning av de två publika webbplatserna genom leverantörens innehållsnätverk byggdes, fungerade och stängdes sedan av igen — eftersom den kostar den enda mening som företagets integritetssida handlar om. Med en proxy framför hanterar ett annat företag varje besökares adress och varje URL som besökaren begär, innan företaget självt gör det, enligt det andra företagets policy och inte den egna. För en verksamhet vars utmärkande påstående är att ingen tittar är det ett sämre byte än de attacker det skyddar mot.</p>\n<p><strong>Resultat.</strong> Zonen är versionshanterad, jämförbar och exporterbar, och det enda beslut som gick emot det självklara standardvalet är nedskrivet med sitt resonemang, så att ingen river upp frågan av misstag. Granskningsvägen är den del som används mest, eftersom det oftare är skillnaden man vill känna till än att täppa till den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Infrastruktur", "Nätverk och VPN", "Python", "Säkerhet", "Infrastruktur som kod", "Nätverk och VPN‑uppsättning", "Systemadministration", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/cut-systemd-sandbox-exposure-across-every-unit-119/", "url": "https://engineer.company/sv/portfolio/cut-systemd-sandbox-exposure-across-every-unit-119/", "title": "Minskade exponeringen i systemd‑sandlådan för varje enhet som plattformen installerar — en tjänst för dödmansgrepp från 9,6 UNSAFE till 1,5, en internetexponerad git‑forge från 8,3 EXPOSED till 1,5 — och lade till en parserkontroll vid konvergering efter att ha hittat ett felstavat direktiv som tyst ignorerades i tre enhetsmallar.", "summary": "Varje enhet körs med de privilegier den behöver och utan dem den inte behöver, siffrorna är uppmätta snarare än påstådda, och den typ av defekt där ett stavfel…", "content_html": "<p><strong>Situation.</strong> Tretton enhetsfiler installerades av det här repot och kördes på produktionsvärden med de privilegier som tjänstehanteraren ger som standard, och det är de flesta. En härdningspoäng gav dödmansgreppets tjänst 9,6 och omdömet osäker; den internetvända git‑forgen fick 8,3 och omdömet exponerad. Båda siffrorna stämde och ingen av dem hade lett till något, eftersom en poäng utan ett tröskelvärde kopplat till sig är en siffra som folk lär sig att hoppa över.</p>\n<p><strong>Uppgift.</strong> Varje enhet behövde en sandlåda i proportion till vad den faktiskt gör, och sandlådorna behövde en kontroll, eftersom en felaktig sandlåda fallerar på så sätt att enheten startar och sedan går sönder någonstans där parsern inte kan se det.</p>\n<p><strong>Handling.</strong> Privilegiebegränsningar fördes in i varje enhetsmall — ett privilegietak låst vid enhetens egen nivå, ett filter för systemanrop, begränsade adressfamiljer och minne som är skrivbart eller körbart men aldrig båda. De uppmätta resultaten: dödmansgreppet gick från 9,6 till 1,5, git‑forgen från 8,3 till 1,5 och båda enheterna för säkerhetskopiering från 9,6 till 2,3 respektive 2,5. När begränsningarna skrevs dök något upp som var bättre än poängen. Ett direktiv hade varit felstavat i tre enhetsmallar så länge de rollerna hade funnits — ett namn som ser trovärdigt ut men inte finns, och som tjänstehanteraren besvarar genom att logga att den inte känner igen nyckeln och sedan starta enheten ändå. Att greppa efter ett direktiv bevisar att det skrevs; bara parsern bevisar att det fick effekt. Därför kör en verifieringsroll nu varje enhet genom tjänstehanterarens egen verifierare under konvergenskörningen, och den inkluderas av varje roll som installerar en enhet. Vad den fortfarande inte kan se står klart utskrivet i samma dokument: varje direktiv som kan få de här enheterna att gå sönder gör det vid körning, inte vid parsning, och en poäng kan inte avgöra om ett skript fortfarande når sin sista rad.</p>\n<p><strong>Resultat.</strong> Varje enhet körs med de privilegier den behöver och utan dem den inte behöver, siffrorna är uppmätta snarare än påstådda, och den typ av defekt där ett stavfel blir en åtgärd som i tysthet saknas får nu en körning att misslyckas. Mätningens begränsningar står antecknade bredvid den, och det är den delen som hindrar nästa läsare från att lita för mycket på ett grönt intervall.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "Infrastruktur", "Linux och servrar", "Säkerhet", "Test och kvalitetssäkring", "Containerisering och orkestrering", "Infrastruktur som kod", "Systemadministration", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/built-seven-read-only-host-reporting-roles-120/", "url": "https://engineer.company/sv/portfolio/built-seven-read-only-host-reporting-roles-120/", "title": "Byggde sju skrivskyddade rapportroller som återger en värd i drift som Markdown — fakta, åtkomst, git, mätvärden, trafik, säkerhet och leverantörsinventering — under regeln att inget tal skrivs ut som körningen inte har mätt.", "summary": "Driftmiljön kan beskrivas utifrån repot på begäran, och rapporterna har hittat verkliga saker: de två döda säkerhetsåtgärderna, en odeklarerad lyssnande port…", "content_html": "<p><strong>Situation.</strong> Värden hade ingen dashboard och skulle inte få någon, eftersom en stack för mätvärden inte ryms på 464 MB och inte hade varit värd sin kostnad om den hade gjort det. Det lämnade en ärlig lucka: det fanns inget sätt att svara på frågor som vem som har åtkomst, hur mycket det här repot har vuxit, hur trafiken ser ut eller om härdningen faktiskt upprätthålls.</p>\n<p><strong>Uppgift.</strong> De frågorna behövde svar som kunde tas fram på begäran, inte kostade värden någonting medan ingen frågade och aldrig kunde ändra det de beskrev.</p>\n<p><strong>Handling.</strong> Sju roller som bara läser och som var och en återger en levande värd som Markdown i repot. En ögonblicksbild av värdens fakta som täcker operativsystem, hårdvara, lagring, nätverk, tjänster, paket, lyssnande socketar och processorbelastning beräknad ur två avläsningar av kärnans egen räknare. En åtkomstrapport som täcker konton, sudo‑omfattning, nyckelfingeravtryck, användare i git‑forgen och databasroller. En git‑rapport per repo. En rapport över mätvärden för dagens insamlade avläsningar. En trafikrapport byggd på webbserverns integritetsmaskerade logg. En leverantörsinventering över fyra leverantörsytor — DNS, två moln‑API:er och ett API för dedikerade servrar. Och en säkerhetsrapport som svarar på om härdningen upprätthålls och inte bara är konfigurerad, och som skriver ut brandväggsreglerna i matchningsordning och den levande regeluppsättningen för spärrar. Den styrande regeln för alla sju är att inget tal skrivs ut som körningen inte har mätt — en rapport som fyller en lucka med en rimlig siffra är sämre än en som lämnar den tom, eftersom det bara är den rimliga siffran som blir trodd.</p>\n<p><strong>Resultat.</strong> Driftmiljön kan beskrivas utifrån repot på begäran, och rapporterna har hittat verkliga saker: de två döda säkerhetsåtgärderna, en odeklarerad lyssnande port och iakttagelsen att trafikarkivet bara är så gammalt som den senaste gången någon kom ihåg att hämta in det. Det sista är nedskrivet som en begränsning med en specifik kostnad angiven — sexton dagars historik för en av webbplatserna som roterades bort mellan inhämtningarna och inte finns någonstans.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Infrastruktur", "Intressenter och rapportering", "Linux och servrar", "Övervakning och observerbarhet", "Systemadministration", "Teknisk dokumentation", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/deployed-the-companys-own-git-forge-121/", "url": "https://engineer.company/sv/portfolio/deployed-the-companys-own-git-forge-121/", "title": "Driftsatte företagets egen git‑forge på Soft Serve, privat som standard, utan webbpanel och med SSH‑porten bunden till loopback bakom en hoppserver, och gjorde landningssidan framför den till en byggartefakt från huvudwebbplatsen i stället för en manuellt underhållen kopia.", "summary": "Företaget hostar sin egen kod på sin egen hårdvara, och sidan framför den ärver varje kontroll som huvudwebbplatsen klarar i stället för att glida isär från…", "content_html": "<p><strong>Situation.</strong> Företagets källkod låg hos en tredjepartstjänst för hosting, vilket är en rimlig plats för den och passar dåligt för en verksamhet vars argument gentemot kunderna är att den inte lämnar ut deras data till mellanhänder. Att i stället köra en git‑forge innebär att köra en git‑forge: autentisering, åtkomstkontroll, lagring, säkerhetskopior och en publik framsida för den.</p>\n<p><strong>Uppgift.</strong> En kanonisk git‑server måste sättas upp på den befintliga värden, med minsta möjliga attackyta och utan webbpanel för administration, och med en landningssida framför den som inte förfaller.</p>\n<p><strong>Handling.</strong> Git‑forgen är en enda binärfil som hålls igång av operativsystemet, installerad från leverantörens eget paketrepo och medvetet vald för att den är byggd i första hand för SSH och saknar administrativt webbgränssnitt — ju mindre gränssnitt, desto mindre att försvara. Den är privat som standard: anonym åtkomst nekas, åtkomst utan nyckel nekas och varje deklarerat repo markeras som privat i stället för att förlita sig på att ingen känner till det. Dess SSH‑lyssnare binder bara till loopback‑gränssnittet på port 23231 och nås utifrån via en hoppserver, så att brandväggens deklarerade yta inte växer. En protokollmultiplexer som skulle lägga HTTPS och git över SSH på en och samma publika port genom att inspektera de första byten i en anslutning är skriven och klar bakom en huvudbrytare, och den brytaren är avslagen: git för flera användare över den publika porten behövs inte än, och en lyssnare som ingen använder är attackyta. Landningssidan framför den var det intressantare problemet. Den hade varit en handunderhållen kopia av huvudwebbplatsens design i ett eget repo, och varenda skillnad som någonsin hittats mellan de två visade sig vara ett misstag snarare än ett beslut: en typskala som återgav ordmärket ungefär nio procent för stort, en typsnittsdeklaration som slog ihop två vikter till en på varje dator där typsnittsfamiljen var installerad, ornament som doldes under en viss bredd så att de saknades på varje telefon, en vikt som användes utan att någon typsnittsfil levererades för den, och inget main‑landmärke eller någon huvudrubrik på någon av sidorna. Fem av fem, och inte en enda syntes i en skärmbild. Kopian togs bort; landningssidan byggs nu av huvudwebbplatsens egna mallar och levereras som en artefakt.</p>\n<p><strong>Resultat.</strong> Företaget hostar sin egen kod på sin egen hårdvara, och sidan framför den ärver varje kontroll som huvudwebbplatsen klarar i stället för att glida isär från den på sätt som bara en mätning kan se. Avvikelser är fortfarande möjliga och måste nu skrivas ned.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Linux och servrar", "Systemadministration", "Webbutveckling", "DevOps och CI/CD‑automatisering", "Infrastruktur som kod", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/", "url": "https://engineer.company/sv/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/", "title": "Driftsatte en containermiljö på Podman och Quadlet under systemd i stället för Docker, eftersom Docker publicerar containerportar ovanför värdbrandväggens egna regler — och gav driftsättningarna en oprivilegierad användare med ett enda fast kommando i stället för root.", "summary": "Containrar körs under den tjänstehanterare som redan var betrodd, bakom den brandvägg som redan var deklarerad, och en rutinmässig release kräver ingen…", "content_html": "<p><strong>Situation.</strong> Plattformen behövde någonstans att köra applikationscontainrar. Det självklara valet var branschens standard, och det självklara valet var fel för den här värden på ett specifikt sätt: det skriver om reglerna i kärnans paketfilter och publicerar containerportar ovanför de brandväggsregler som härdningsrollen installerar, så att en container i tysthet blir nåbar från internet oavsett vad brandväggen har instruerats att göra.</p>\n<p><strong>Uppgift.</strong> Ett containerskikt måste väljas som inte kringgår brandväggen, inte lägger till en andra tjänstehanterare bredvid den som redan är betrodd och inte kräver att en person är root för att göra en release.</p>\n<p><strong>Handling.</strong> Skiktet är en containermotor utan daemon som driver enheter genererade av operativsystemets egen tjänstehanterare. Det finns ingen andra processhanterare: containrar är tjänster, de startar på samma sätt som tjänster startar och de beskrivs i samma deklarativa form som allt annat. Containrarna använder värdnätverk utan några publicerade portar alls, vilket tar bort frågan om att kringgå brandväggen i stället för att mildra den. Enheterna körs på systemnivå snarare än rootless, och det är nedskrivet som en avvägning — det håller automatiseringen enkel och normal, till priset av den striktare isolering som rootless skulle ge, och anteckningen säger åt vilket håll man ska gå om containerutbrytning någon gång blir viktigare än en enkel automatisering. Driftsättning skildes från provisionering: root förbereder driftsättningsvägen en gång, och därefter driftsätter en oprivilegierad användare på nytt genom att köra ett enda fast skript via en avgränsad regel som tillåter just det kommandot och inga argument. Bygg avbildningen, starta om tjänsten. Inget root‑skal, inga godtyckliga kommandon.</p>\n<p><strong>Resultat.</strong> Containrar körs under den tjänstehanterare som redan var betrodd, bakom den brandvägg som redan var deklarerad, och en rutinmässig release kräver ingen privilegierad åtkomst. Valet mot standarden är nedskrivet med sitt skäl, vilket väger tyngre än själva valet — nästa person kommer att få höra att standarden ska användas i varenda artikel som den personen läser.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Containrar (Docker/Kubernetes)", "DevOps", "Infrastruktur", "Linux och servrar", "Säkerhet", "Containerisering och orkestrering", "DevOps och CI/CD‑automatisering", "Molninfrastruktur och migrering", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/provisioned-a-second-server-from-code-123/", "url": "https://engineer.company/sv/portfolio/provisioned-a-second-server-from-code-123/", "title": "Automatiserade provisioneringen av en andra server hos en andra molnleverantör och skapade brandväggen före maskinen så att den föds bakom en, med båda leverantörernas brandväggar skrivna direkt mot deras REST‑API:er för att undvika en collection från tredje part.", "summary": "En andra värd kan skapas, eller tas över på nytt, från repot, bakom en brandvägg som redan finns, till en kostnad som filen anger.", "content_html": "<p><strong>Situation.</strong> En andra maskin behövdes för ett applikationsskikt, hos en annan leverantör än den första, och den första maskinens egen historik var argumentet för hur det skulle göras: den hade skapats för hand och dess brandvägg hade lagts till i efterhand, vilket lämnar ett fönster där en ny värd med en standardpolicy för lösenord är nåbar från internet.</p>\n<p><strong>Uppgift.</strong> Maskinen måste skapas från repot, och den måste födas bakom sin brandvägg i stället för att få en strax efteråt.</p>\n<p><strong>Handling.</strong> Ordningsföljden bar designen. Brandväggens play körs före serverns play, så att regeluppsättningen finns innan det finns något för den att skydda; hos den andra leverantören körs taggningen före molnbrandväggen, eftersom en brandvägg som riktar sig mot taggar kräver att taggarna redan finns. Båda leverantörernas brandväggar skrivs direkt mot deras REST‑API:er genom en generisk HTTP‑task i stället för genom en collection från leverantören, vilket tar bort ett beroende och dess versionsglidning till priset av att förfrågningarnas form skrivs för hand. Rollen som skapar maskinen tar över en befintlig i stället för att duplicera den om den redan finns, så att det är säkert att köra om. Den är också dokumenterad, i en egen fil, som den enda rollen i repot som kostar pengar — instanstypen, regionen, specifikationen och månadskostnaden i euro är nedskrivna, eftersom en play som fakturerar någon ska säga det där den personen kommer att läsa det. Den playen är också den som inte kan provköras på vanligt sätt: den generiska HTTP‑tasken deklarerar inget stöd för check mode, så en torrkörning hoppar över varje task i den. Provkörningen görs i stället mot brandväggens play, som är gratis och går att ångra.</p>\n<p><strong>Resultat.</strong> En andra värd kan skapas, eller tas över på nytt, från repot, bakom en brandvägg som redan finns, till en kostnad som filen anger. Det enda som en torrkörning inte kan täcka är namngivet i samma fil i stället för att lämnas som en överraskning.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Infrastruktur", "Molntjänster", "Python", "Säkerhet", "Infrastruktur som kod", "Molninfrastruktur och migrering", "Nätverk och VPN‑uppsättning", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/", "url": "https://engineer.company/sv/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/", "title": "Skrev in en avgränsningsregel i repot efter att en omstrukturering hade fört in ett annat företags värdförteckning, brandväggsundantag och text i det — och höll de karantänsatta resterna kvar under skanningen efter hemligheter i stället för att undanta dem.", "summary": "Omfattningsgränsen är en nedskriven regel med ett angivet test, resterna är synliga i stället för begravda, och den specifika form av inloggningsuppgift som…", "content_html": "<p><strong>Situation.</strong> Arbete som hade gjorts åt ett annat företag följde med genom en omstrukturering av repot och blev kvar. Det som följde med var inte abstrakt: en kladdlogg med aktiva inloggningsuppgifter i klartext, som låg kvar i trädet förbi varje skydd under större delen av repots historik; en inaktuell inventory som namngav det företagets värd; och en task‑fil för brandväggen som öppnade portar mot sexton av dess kundnätverk. Inget av det kördes. Det är precis därför det överlevde — ingenting som inte körs någonstans granskas någonsin igen.</p>\n<p><strong>Uppgift.</strong> Gränsen måste bli en regel med ett test kopplat till sig, snarare än en avsikt, och resterna måste hanteras på ett sätt som inte bara gömde undan dem.</p>\n<p><strong>Handling.</strong> Regeln är nu det inledande avsnittet i repots instruktioner: det här repot hanterar ett enda företags infrastruktur och bara den, och en annan parts värd, inventory, brandväggsöppning, DNS‑zon eller inloggningsuppgift hör inte hemma i det — inte ens avaktiverad, bortkommenterad eller parkerad i en fil som ingen playbook importerar. Det praktiska testet står bredvid: skulle det här företaget fortfarande vara ansvarigt för detta om relationen tog slut. Resterna sattes i karantän i en tydligt namngiven katalog i stället för att raderas, så att historiken förblir läsbar, och karantänen är medvetet partiell — de två strukturella lintverktygen hoppar över den, medan hemlighetsskannern, vault‑skyddet och emojikontrollen medvetet fortfarande läser den, eftersom det är de tre som skulle fånga det som tog sig in. Läckan i sig gav upphov till en lintregel: ett eget mönster som flaggar en inloggningsuppgift som skickas som en kommandoradsflagga, vilket var den form den läckta hade och som den vanliga regeluppsättningen inte matchade.</p>\n<p><strong>Resultat.</strong> Omfattningsgränsen är en nedskriven regel med ett angivet test, resterna är synliga i stället för begravda, och den specifika form av inloggningsuppgift som tog sig igenom får nu en commit att misslyckas. Lärdomen som har dokumenterats vid sidan av är den allmänna: den farliga artefakten är inte den som körs, utan den som inte gör det, eftersom det är den som ingen läser igen.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Datastyrning", "Dokumentation", "Infrastruktur", "Säkerhet", "Datastyrning och datakvalitet", "Säkerhet och behörighetshantering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/split-every-operational-secret-per-host-125/", "url": "https://engineer.company/sv/portfolio/split-every-operational-secret-per-host-125/", "title": "Delade upp de fyra driftshemligheterna per värd efter att ha konstaterat att två värdar som delar ett dödmansgrepp larmar mindre än två separata grepp, inte mer, och att en delad lösenfras för säkerhetskopior gör två värdar till ett enda repo.", "summary": "Hemligheterna är per värd genom själva konstruktionen, och de två fellägen som skulle ha följt av att dela dem är nedskrivna där nästa person redigerar filen.", "content_html": "<p><strong>Situation.</strong> Plattformen byggdes för en värd och skulle snart ha två. Flera operativa hemligheter hade skrivits som enskilda värden utifrån det antagandet: ett repo för säkerhetskopior, en lösenfras, en larmbrytare. Att utvidga dem till en andra värd genom att dela dem är minsta motståndets väg, och det är fel på två separata sätt, som båda misslyckas i tysthet.</p>\n<p><strong>Uppgift.</strong> Varje hemlighet måste bli en mappning med en nyckel per värd, och skälet måste skrivas ned, eftersom den delade varianten ser korrekt ut och inte kostar något förrän den dag det spelar roll.</p>\n<p><strong>Handling.</strong> Två argument avgjorde saken, och båda står nedskrivna bredvid konfigurationen. Ett dödmansgrepp som delas av två värdar larmar mindre än två separata grepp, inte mer: så länge någon av värdarna fortfarande pingar förblir kontrollen grön medan den andra är död, och att lägga till en värd i ett delat grepp minskar därför aktivt täckningen för den som redan fanns. Och en sträng för ett repo för säkerhetskopior är bara en plats — lösenfrasen är hela krypteringen, så två värdar som delar en lösenfras är inte två repon med en gemensam hemlighet, utan ett repo med två kataloger i. Varje värde blev en mappning med inventory‑värdnamnet som nyckel, uppslagen per värd av en delad task‑fil som både konvergenskörningen och förhandskontrollen kör. Den befintliga värden namngavs sedan i alla fyra mappningarna med sitt repo och båda larmbrytarna lämnade tomma, vilket är det sanningsenliga tillståndet och håller de två öppna initiativen ärliga om att det som återstår är operatörens halva av arbetet och inte koden.</p>\n<p><strong>Resultat.</strong> Hemligheterna är per värd genom själva konstruktionen, och de två fellägen som skulle ha följt av att dela dem är nedskrivna där nästa person redigerar filen. De tomma posterna är den del som är värd att behålla: de säger att kopplingen finns och att värdet inte gör det, vilket är ett annat och mer användbart besked än en saknad nyckel.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Infrastruktur", "Linux och servrar", "Lösningsarkitektur", "Säkerhet", "Tillförlitlighet och säkerhetskopiering", "Infrastruktur som kod", "Säkerhet och behörighetshantering", "Säkerhetskopiering och återställning", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-multilingual-static-site-in-hugo-126/", "url": "https://engineer.company/sv/portfolio/built-a-multilingual-static-site-in-hugo-126/", "title": "Byggde en flerspråkig statisk webbplats i Hugo med 108 mallfiler, varav 53 partials, som publicerar varje sida i fyra representationer från ett innehållsträd på tre språk, och en gång till under sju fokuserade underdomäner byggda från samma träd.", "summary": "Webbplatsen levererar tre språk och fyra representationer från ett och samma träd, har ingen backend att angripa och inget att fakturera, och den enda biten…", "content_html": "<p><strong>Situation.</strong> Företaget behövde en publik webbplats som fungerar på tre språk, bevisar förmåga i stället för att påstå den, inte kostar något att driva och inte lämnar ut sina besökare till någon. De flesta av dem är vanliga krav. Tillsammans utesluter de nästan varje system för innehållshantering, eftersom en backend som körs på servern är något som ska säkras, patchas, betalas för och förklaras på en integritetssida.</p>\n<p><strong>Uppgift.</strong> Webbplatsen måste genereras helt vid byggtid och ändå bete sig som en modern webbplats — sökbar, installerbar, syndikerad, utskrivbar och läsbar för en skärmläsare på varje språk den levereras på.</p>\n<p><strong>Handling.</strong> Det är en statisk webbplats: 108 mallfiler, varav 53 är partials och 18 är shortcodes, tre språk, ingen backend på servern och ett enda förstapartsskript som har beviljats som ett avgränsat undantag för ett portföljfilter vars alla reglage är dolda tills det körs. Förmåga läggs till vid byggtid i stället för i webbläsaren, vilket är ett produktbeslut och är nedskrivet som ett sådant. Varje sida publiceras i fyra representationer från ett och samma innehållsträd — HTML, en Markdown‑tvilling, ett Gemini‑dokument och en post i en Gopher‑meny — och startsidan lägger dessutom till tre syndikeringsflöden och ett manifest för en installerbar applikation. Taxonomierna är medvetet två i stället för en, och distinktionen är bärande: en kategori är en ämnestagg på arbetet, en tjänst är något som företaget säljer, och att slå ihop dem skulle ha gjort katalogen till en lista över färdigheter i stället för en lista över erbjudanden. Samma träd byggs sedan sju gånger till, en gång per fokuserad underdomän, genom att generatorn pekas mot en annan innehållskatalog i stället för att något forkas.</p>\n<p><strong>Resultat.</strong> Webbplatsen levererar tre språk och fyra representationer från ett och samma träd, har ingen backend att angripa och inget att fakturera, och den enda biten JavaScript i den hålls till ett kontrakt som ett lintverktyg upprätthåller. Kostnaden är att allt interaktivt måste lösas vid byggtid eller inte alls, vilket har uteslutit flera saker som hade varit enkla med en server och är skälet till att sökindexet kostnadsberäknades och parkerades i stället för att levereras.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Frontendutveckling", "Fullstackutveckling", "Internationalisering", "Prestandaoptimering", "Webbutveckling", "Fullstackutveckling av produkter", "Internationalisering och lokalisering", "Redo för AI‑agenter", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-22-linter-commit-gate-127/", "url": "https://engineer.company/sv/portfolio/built-a-22-linter-commit-gate-127/", "title": "Byggde en commit‑grind med 22 linters på en rad var plus fem som förtjänar ett eget stycke, utan varningsnivå och utan tillåtna inline‑undantag, som täcker HTML, CSS, JavaScript, Python, YAML, Markdown, shell, länkar, stavning, hemligheter och typografi.", "summary": "Mekaniska invändningar görs av en maskin innan en commit ens finns, och grinden är den enda granskare som projektet har.", "content_html": "<p><strong>Situation.</strong> Riktlinjer för bidrag är en uppsättning förslag. Alla håller med om dem, och sedan är det fredag, ändringen är liten och riktlinjerna förlorar. I ett enmansprojekt är det värre snarare än bättre, eftersom det inte finns någon granskare alls — det enda som står mellan en dålig ändring och produktion är personen som skrev den, i det ögonblick då den personen är minst benägen att argumentera med sig själv.</p>\n<p><strong>Uppgift.</strong> Standarderna måste vara körbara, så att ett brott mot en av dem får en commit att misslyckas i stället för att vänta på att bli upptäckt.</p>\n<p><strong>Handling.</strong> Det som växte fram är en grind med 22 lintverktyg utan någon varningsnivå: varje diagnostik är ett fel, och själva webbplatsgeneratorn körs med varningar uppgraderade till fel, så att även en utfasningsvarning stoppar bygget. De självklara finns där — HTML‑validering, CSS, JavaScript, Python, YAML, Markdown, skalskript, stavning, hemligheter, döda länkar. De intressanta är de projektspecifika kontrollerna som inget färdigt verktyg har någon åsikt om: att båda färgteman målar varje skiktad yta med samma antal lager, att inget fotografi visas bredare än hälften av källbildens pixlar, att det driftsatta trädet inte innehåller någon privat sökväg eller något värdnamn, att prosan följer tonreglerna på alla tre språken, att en sida har en Markdown‑tvilling och en giltig representation för alternativa protokoll. Undertryckningar direkt i koden är helt förbjudna — ingen ignore‑kommentar, inget disable‑direktiv, ingen förbikoppling av hooken och inget namnbyte på en fil för att slippa undan en matchare. Regeln som håller det ärligt är att ett redan befintligt fel inte är någon ursäkt: en defekt som ingen införde men som en kontroll lyfter fram rättas i samma vända.</p>\n<p><strong>Resultat.</strong> Mekaniska invändningar görs av en maskin innan en commit ens finns, och grinden är den enda granskare som projektet har. Kostnaden redovisas i stället för att döljas: att committa går långsamt, och ett illa skrivet skydd är riktigt frustrerande att arbeta runt, vilket är skälet till att skydden själva senare fick en egen formaterare och ett eget lintverktyg.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Frontendutveckling", "Python", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Produktstrategi och krav", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/", "url": "https://engineer.company/sv/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/", "title": "Kortade webbplatsens webbläsarstyrda kvalitetsgrind från 1 636 sekunder till 615 genom att schemalägga kontrollerna med den längsta först i en workerpool begränsad till fyra spår, efter att ha mätt att alfabetisk ordning kostade 320 sekunder mot 224.", "summary": "Grinden gick från 1 636 sekunder till 615, samtidigt som kontrollerna blev bredare snarare än tunnare — den seriella kostnaden steg och väggklockstiden sjönk.", "content_html": "<p><strong>Situation.</strong> Elva av webbplatsens kontroller styr en huvudlös webbläsare: layout vid varje fönsterform som designen är ritad för, typskala, expansion av översatt text, kontrast, tillgänglighetsregler, tvingade färger, maskotens storlek, rörelse, utskrift i sex papperskombinationer, konsolfel och visuell regression. Körda en i taget tog de 1 636 sekunder, drygt tjugosju minuter. En grind som tar tjugosju minuter är en grind som hoppas över, och en kontroll som hoppas över går inte att skilja från en som går igenom.</p>\n<p><strong>Uppgift.</strong> Väggklockstiden måste ned så långt att det blev standard att köra dem snarare än ett beslut, utan att någon av kontrollerna försvagades.</p>\n<p><strong>Handling.</strong> Arbetet började med mätning. Varje kontroll tidtogs för sig på en maskin med tolv kärnor: layout på 405 sekunder, typografi på 301, översättning på 282, kontrast på 189, tillgänglighet på 178 och så vidare ned till sjutton. Två iakttagelser formade svaret. Att köra alla på en gång var långsammare än att köra fyra åt gången — 265 sekunder mot 224 — eftersom varje kontroll i sig är en webbläsare som arbetar parallellt, och att överbelasta maskinen kostar mer än samtidigheten vinner. Och att ordna dem efter längsta bearbetningstid först slog alfabetisk ordning med nästan en tredjedel, 224 sekunder mot 320, vilket är det klassiska resultatet inom schemaläggning och syns här eftersom kontrollernas kostnad skiljer sig med en faktor tjugo. Därför är körverktyget en begränsad pool av arbetsprocesser, dimensionerad efter antalet kärnor med ett golv på två och ett tak på fyra, och matad med de längsta först. Samtidigt breddades kontrollerna i stället för att smalnas av: de delar nu en viewport‑tabell med tjugotvå fönsterformer, härledd ur varje media query som stilmallen faktiskt innehåller, vilket ensamt tog layoutkontrollen från 95 sekunder till 405.</p>\n<p><strong>Resultat.</strong> Grinden gick från 1 636 sekunder till 615, samtidigt som kontrollerna blev bredare snarare än tunnare — den seriella kostnaden steg och väggklockstiden sjönk. Mätningen är den del som är värd att behålla: två val som låter rimliga, att köra allt på en gång och att köra saker i den ordning de skrevs, var vart och ett mätbart sämre än alternativet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Frontendutveckling", "Prestandaoptimering", "Python", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/", "url": "https://engineer.company/sv/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/", "title": "Ersatte 27 renderade teckenstorlekar, vars närmaste grannar låg 0,6 % från varandra, med en Major Third‑skala i sex steg, och skrev lintern som fäller den tjugoåttonde.", "summary": "Sex storlekar där det fanns tjugosju, med ett angivet förhållande, en angiven radlängd och en kontroll som avvisar nästa oplanerade värde.", "content_html": "<p><strong>Situation.</strong> En mätning av webbplatsens renderade text hittade tjugosju olika teckenstorlekar i bruk. Flera skilde sig åt med mindre än en procent — fem värden mellan 0,8 och 0,85 av brödtextens storlek var i bruk samtidigt, vilket är en skillnad som ingen läsare kan uppfatta och som varje framtida redaktör kommer att fylla på. Två av rubrikerna fick ingen storlek alls av stilmallen och föll igenom till webbläsarens egna standardvärden, ett förhållande på 1,33 som inte stämde med något annat på sidan.</p>\n<p><strong>Uppgift.</strong> Storlekarna måste bli en skala med ett angivet förhållande, och något måste förhindra den tjugoåttonde.</p>\n<p><strong>Handling.</strong> Skalan är en stor ters, med förhållandet 1,25, sex namngivna steg från finstilt till displaytext, vart och ett en token snarare än ett värde. Rubrikerna mappas uttryckligen på steg i stället för att ärva det som webbläsaren tycker, vilket är det som rättade de två som inte hade någon egen storlek. Bara det nedersta steget har ett golv, så att finstilt förblir läsbart på en telefon utan att hela skalan låses fast. Logotyper undantas med namn snarare än av en slump. Lintverktyget är det som får det att hålla: det renderar webbplatsen och ger fel vid den tjugoåttonde olika storleken. Det har redan gjort sig förtjänt av sin plats två gånger — det fångade en rubrik som hamnade på 1,17 av brödtextens storlek, vilket inte är ett steg i någonting, och namngav förhållandet i felmeddelandet; och det fångade inline‑kod på 0,9, vilket är precis den sortens värde som skalan finns till för att förhindra. Tillsammans med skalan kom en radlängd på ungefär sjuttiotvå tecken, som ersatte en ärvd fast bredd som gav nittioen tecken på en genomgångssida och hundratio på kontaktsidan.</p>\n<p><strong>Resultat.</strong> Sex storlekar där det fanns tjugosju, med ett angivet förhållande, en angiven radlängd och en kontroll som avvisar nästa oplanerade värde. Begränsningen är verklig och ibland obekväm: en design som vill ha en storlek mellan två steg måste flytta till ett steg eller argumentera för att ändra skalan, och den diskussionen har förts och förlorats mer än en gång.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Dokumentation", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "UI/UX‑design och designsystem", "Varumärke, marknadsföring och SEO" ] }, { "id": "https://engineer.company/sv/portfolio/took-wcag-2-2-level-aa-across-the-whole-site-130/", "url": "https://engineer.company/sv/portfolio/took-wcag-2-2-level-aa-across-the-whole-site-130/", "title": "Förde 32 representativa sidor — en per mall och språk — till WCAG 2.2 nivå AA i båda färgteman, plus två AAA‑kriterier, med en dokumenterad överensstämmelseredovisning och en kontroll som försvarar varje påstående.", "summary": "Överensstämmelse hävdas på nivå AA på tre språk och i två teman, med två AAA-kriterier utöver det och ett angivet som undantag, och varje påstående har en…", "content_html": "<p><strong>Situation.</strong> En konsultfirma som säljer ingenjörsmässigt omdöme och levererar en otillgänglig webbplats har ett trovärdighetsproblem innan den har ett tillgänglighetsproblem. Webbplatsen har dessutom en ovanligt bred yta för det: tre språk, två färgteman, en fullständig stilmall för utskrift, en palett som utgår från mörkt läge, handritade annoteringar och en illustrerad figur — vart och ett av dem ett sätt att misslyckas med ett kriterium i en konfiguration och klara det i en annan.</p>\n<p><strong>Uppgift.</strong> Webbplatsen måste uppfylla WCAG 2.2 på nivå AA på varje sida, på varje språk och i båda temana, och överensstämmelsen måste försvaras av kontroller snarare än påstås i ett dokument.</p>\n<p><strong>Handling.</strong> En förteckning över överensstämmelse dokumenterar varje kriterium som omfattas, där varje rad anger vad som uppfyller det och vad som kontrollerar det. Två kriterier på nivå AAA uppfylls utöver målet — visuell presentation i sin helhet, och fokusutseende, som erbjuds snarare än hävdas. Automatiseringen kör en regelmotor för tillgänglighet med reglerna för bästa praxis aktiverade i stället för bara reglerna för överensstämmelse, vilket är en skillnad på trettio regler och spelade roll direkt: en av de extra reglerna fallerade i samma ögonblick som den slogs på, eftersom varumärkessymbolen låg utanför alla landmärken på varje undersida. En andra, långsammare kontroll kör trettiotvå representativa sidor över två färgscheman och två bredder, och det den kontrollerar är ovanligt: fokus bevisas i pixlar snarare än i dokumentet, genom att tabbtangenten trycks ned på riktigt och skärmbilder jämförs, eftersom identiska pixlar betyder att användaren inte kan se var fokus är, oavsett vad uppmärkningen säger. Den går också igenom hela sidan på jakt efter en tangentbordsfälla, tillämpar de angivna åsidosättningarna av textavstånd, fördubblar rotens teckenstorlek och mäter radlängd, marginaljustering, centrering och styckeavstånd.</p>\n<p><strong>Resultat.</strong> Överensstämmelse hävdas på nivå AA på tre språk och i två teman, med två AAA‑kriterier utöver det och ett angivet som undantag, och varje påstående har en kontroll bakom sig. Det svåraste av dem var kontrast över fotografiet, som en automatiserad regelmotor rapporterar som omöjlig att utvärdera och som webbplatsen hade försvarat med ett argument snarare än en siffra: de faktiska siffrorna var 49 av 64 par av sida och tema under AA. Två ytor som designen aldrig hade haft kontroll över visade sig vara orsaken, och kontrollen som hittade dem är nu en lägstanivå — varje par måste klara AA över väggen som bygget ritar, och varje par som inte gör det får bygget att misslyckas. Det som webbplatsen säger på sin egen sida med erkännanden är den ärliga delen: inget automatiserat verktyg hittar mer än ungefär en tredjedel av WCAG‑felen, etiketterna som visas vid hovring kan inte stängas med en tangent eftersom det kräver ett skript och den här webbplatsen kör ett enda, och det har inte gjorts några tester med riktiga hjälpmedel — de begränsningarna står där en läsare kommer att se dem i stället för att utelämnas ur påståendet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Dokumentation", "Frontendutveckling", "Internationalisering", "Test och kvalitetssäkring", "UX/UI‑design", "Webbutveckling", "Internationalisering och lokalisering", "Redo för AI‑agenter", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/fixed-11-accessibility-defects-the-browser-called-healthy-131/", "url": "https://engineer.company/sv/portfolio/fixed-11-accessibility-defects-the-browser-called-healthy-131/", "title": "Hittade och åtgärdade 11 tillgänglighetsbrister som webbläsaren rapporterade som felfria — åtta sidfotslänkar som låg kvar i tabbordningen bakom pointer‑events, en scroll‑tidslinje som klippte bort kolofonen på tre sidor och en regelmotor som körde 70 av sina 105 regler.", "summary": "Elva fel åtgärdade och, mer användbart, fyra regler som överlever dem: en kontroll som testar en axel försvarar en axel, en regeluppsättning som inte…", "content_html": "<p><strong>Situation.</strong> Webbplatsen klarade sina automatiska tillgänglighetsregler. Den hade också elva fel som de reglerna inte kunde se, eftersom vart och ett var en egenskap hos hur sidan renderades snarare än hos vad uppmärkningen sade — den sortens fel som ett valideringsverktyg rapporterar som felfritt och som en tangentbordsanvändare stöter på inom några sekunder.</p>\n<p><strong>Uppgift.</strong> Felen skulle hittas, åtgärdas och föras in i en förteckning tillsammans med den regel som vart och ett gav upphov till, så att det är felklassen som stängs och inte bara det enskilda fallet.</p>\n<p><strong>Handling.</strong> De föll i tre grupper. Storlek: ett nyckelord i stilmallen som användes som om det vore relativt låste en hel region vid sexton pixlar medan brödtexten gick upp till tjugotvå; ett element för nedsänkt text användes i betydelsen ”mindre”; när texten krympte blev raderna längre, tills en rubrik nådde nittiosex tecken; och ett uppmätt höjdtak räckte inte till för just den justering av textavståndet som webbplatsen säger sig stödja. Fokus och beskärning: åtta länkar i sidfoten låg kvar i tabbordningen bakom en egenskap som tar bort pekarinteraktion och ingenting annat; en overflow‑regel skapade en rullningsbehållare som ingen ville ha; och en rullningsstyrd animation, som helt enkelt är inaktiv på en sida som är för kort för att rullas, skar permanent bort sidfoten på tre sidor. Och två fel i själva kontrollerna, som är de som är värda att nämna — varje fönsterformat som layoutkontrollen öppnade var niohundra pixlar högt, så en telefon i liggande läge på 852 × 393 gav sju pixlar mellan två element utan att någon kontroll någonsin hade tittat; och regelmotorn hade kört sjuttio av sina hundrafem regler, så den regel som skulle ha fångat det saknade landmärket fanns inte med i uppsättningen.</p>\n<p><strong>Resultat.</strong> Elva fel åtgärdade och, mer användbart, fyra regler som överlever dem: en kontroll som testar en axel försvarar en axel, en regeluppsättning som inte innehåller regeln kan inte upprätthålla den, höjd är en dimension, så testa den, och bevisa synlighet i pixlar snarare än i dokumentet. När layoutkontrollen öppnades på rätt sätt igen fallerade den hundratjugoåtta gånger på tre språk, bland annat i ett telefonstort fönster där hero‑sektionens avslutande rad hamnade tjugoen pixlar nedanför skärmkanten.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/", "url": "https://engineer.company/sv/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/", "title": "Genererade 495 resultatsidor på tre språk från en skrivskyddad SQLite‑export, med sidadressen angiven som data så att en rättad mening inte längre flyttade sidan och bröt länken.", "summary": "Fyrahundranittiofem sidor genereras från en källa, att rätta ett påstående kostar ingenting och de adresser som webbplatsen någonsin har publicerat fortsätter…", "content_html": "<p><strong>Situation.</strong> Företagets portföljinnehåll skrivs i en separat databas — samma som producerar CV:t — och webbplatsen måste publicera det som sidor, på tre språk, utan att de två kopiorna glider isär. Det naiva angreppssättet, att skriva sidorna för hand och hålla dem i takt, faller vid den första rättelsen.</p>\n<p><strong>Uppgift.</strong> Webbplatsen måste generera sitt innehåll från databasen som indata till bygget, med sidadresser som överlever att meningarna på sidorna skrivs om.</p>\n<p><strong>Handling.</strong> En exportör läser databasen i skrivskyddat läge och skriver en sida per resultat och språk — 495 sidor — plus de taxonomi- och tjänstedata som mallarna behöver. Den använder bara standardbiblioteket, så webbplatsens bygge har inget beroende av generatorns miljö, och eftersom utdata checkas in kan webbplatsen byggas fristående. Det enskilt mest betydelsefulla beslutet i den gäller adresser. Webbplatsen härledde tidigare en sidas URL från de inledande orden i dess engelska påstående, så en rättad mening flyttade sidan i tysthet och bröt varje länk till den — en webbplats vars argument för sig själv är att den rättar saker fick betala med en död länk varje gång den gjorde det. Adressen skrivs nu som data: en rad per adress och resultat, där den första är aktuell och varje senare är en avvecklad adress som webbplatsen publicerar som en omdirigering. Två andra fällor från samma arbete finns dokumenterade. En jämförelse av en period mot en textkolumn matchade i tysthet alla trettiotvå rader på grund av hur databasen tilldelar typaffinitet i en jämförelse. Och språklistan är nu den enda axel som skrivslingan, datafilerna och databasfrågorna alla härleds från, så att lägga till ett fjärde språk är en enda post i en mappning snarare än en sökning.</p>\n<p><strong>Resultat.</strong> Fyrahundranittiofem sidor genereras från en källa, att rätta ett påstående kostar ingenting och de adresser som webbplatsen någonsin har publicerat fortsätter att svara. Exportören äger också exakt ett presentationsbeslut — hur tjänsterna grupperas i tematiska avsnitt — och det är avsiktligt: allt annat den skriver tillhör databasen, och en grupprubrik som saknar ett språk får exporten att fallera i stället för att engelska renderas ovanpå översatt innehåll.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Databaser", "Datapipelines (ETL/ELT)", "Frontendutveckling", "Internationalisering", "Python", "SQL", "Webbutveckling", "Internationalisering och lokalisering", "Utveckling av datapipelines (ETL/ELT)", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/closed-the-colour-system-at-28-colours-133/", "url": "https://engineer.company/sv/portfolio/closed-the-colour-system-at-28-colours-133/", "title": "Stängde färgsystemet vid 28 dokumenterade färger med en linter som fäller ett värde som målas men inte är dokumenterat, dokumenteras men inte målas, är felmätt, skrivs om som en literal eller ligger inom ett perceptuellt avstånd på 0,02 från en befintlig färg.", "summary": "Paletten är en sluten, uppmätt uppsättning som inte kan växa i det tysta, och de två nästan identiska stavningarna som föranledde arbetet är borta.", "content_html": "<p><strong>Situation.</strong> Färg på en webbplats med ett mörkt standardtema, ett ljust tema, en utskriftsstilmall, ett läge för tvingade färger och ett illustrerat varumärke förblir inte en liten uppsättning av sig själv. Den hade redan börjat glida på det sätt den alltid gör: två stavningar av samma färg som låg så nära varandra att ingen kunde skilja dem åt, värden som skrivits in på nytt som literaler bredvid de tokens som definierade dem, och dokumenterade färger som ingenting ritade ut.</p>\n<p><strong>Uppgift.</strong> Paletten måste bli en sluten uppsättning med ett angivet tröskelvärde för särskiljbarhet, och något måste se till att den förblev sluten.</p>\n<p><strong>Handling.</strong> Tjugoåtta färger, var och en dokumenterad med det kontrastförhållande den uppmäts till på den yta där den förekommer, och elva varumärkesvärden deklarerade en gång som tokens. Tröskelvärdet är numeriskt snarare än redaktionellt: två färger som ligger närmare varandra än ett perceptuellt avstånd på 0,02 i en likformig färgrymd är en färg med två stavningar. Lintern fallerar på fem olika sätt — ett värde som ritas ut men inte är dokumenterat, som är dokumenterat men inte ritas ut, som är registrerat med fel förhållande, som ligger inom tröskelvärdet från ett som redan finns, eller ett varumärkesvärde som stavats om som en literal. Den hittade två direkt: en temafärg som fanns i två stavningar med 0,018 emellan, och en bakgrundsfärg som stod i stället för en vägg den låg 0,021 ifrån. Transparens uttrycks med relativ färgsyntax i stället för med blandningsfunktionen, uttryckligen för att kontrollen inte kan se igenom en blandning, och en palettkontroll som går att kringgå är ingen kontroll. Regeln som hör till är kort: ändra en token snarare än en regel, lägg bara till en färg när ingen passar, och sänk aldrig ett dokumenterat kontrastförhållande för att få en design att fungera.</p>\n<p><strong>Resultat.</strong> Paletten är en sluten, uppmätt uppsättning som inte kan växa i det tysta, och de två nästan identiska stavningarna som föranledde arbetet är borta. Det är en begränsning som ibland säger nej — en design som vill ha en något annorlunda blå nyans måste ta den som finns eller argumentera för en tjugonionde färg, och det argumentet måste innehålla kontrastförhållandet.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Dokumentation", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "UI/UX‑design och designsystem", "Varumärke, marknadsföring och SEO" ] }, { "id": "https://engineer.company/sv/portfolio/found-the-light-theme-missing-an-overlay-layer-134/", "url": "https://engineer.company/sv/portfolio/found-the-light-theme-missing-an-overlay-layer-134/", "title": "Upptäckte att det ljusa temat hade saknat ett helsidesöverlägg sedan den dag det skrevs, genom att kräva att båda teman målar varje skiktad yta med samma antal skikt.", "summary": "Ett lager som saknas i ett av temana stoppar nu en commit, och ett som hade saknats sedan det skrevs ritas nu ut.", "content_html": "<p><strong>Situation.</strong> Webbplatsen levereras med två färgteman. Det mörka är standard och tittas på hela tiden; det ljusa är en åsidosättning som bara visas under en viss systeminställning, vilket betyder att det ses betydligt mer sällan och aldrig av någon som kontrollerar det. Temaparitet är en felklass där en yta byggs en gång och deklareras på nytt en gång, och den nya deklarationen tappar i tysthet ett lager.</p>\n<p><strong>Uppgift.</strong> Pariteten behövde säkerställas maskinellt, eftersom det enda alternativet är att en person kommer ihåg att byta inställning och titta.</p>\n<p><strong>Handling.</strong> Kontrollen är liten och strukturell: för varje skiktad yta räknas de lager som varje tema ritar ut, och den fallerar när antalen skiljer sig åt. Den jämför inte utseenden — teman ska se olika ut — utan sammansättning, som är det som ska vara identiskt. Den hittade felet vid första körningen. Webbplatsens helsidesomslag består av tre lager i det mörka temat, ett fasettmönster över en scrim över väggen, och det ljusa temat deklarerade det på nytt med två. Fasettlagret hade saknats i det ljusa temat från den dag det skrevs, och ingenting hade någonsin påpekat det, eftersom en sida som saknar ett av tre bakgrundslager ser ut som ett designbeslut snarare än en bugg. Åtgärden var en enda regel; dokumentationen efteråt registrerade vad lagret kostar i kontrast, så att tillägget är uppmätt snarare än antaget att vara gratis.</p>\n<p><strong>Resultat.</strong> Ett lager som saknas i ett av temana stoppar nu en commit, och ett som hade saknats sedan det skrevs ritas nu ut. Det är just den typ av fel som hela kontrollansatsen är inriktad på — ingenting var trasigt, ingenting gav felmeddelanden, sidan renderades korrekt, och den hade varit fel i månader.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/made-reduced-motion-honest-135/", "url": "https://engineer.company/sv/portfolio/made-reduced-motion-honest-135/", "title": "Fick reduced‑motion att hålla vad den lovar efter att ha hittat elva selektorer som fortfarande animerade under inställningen, eftersom en universell transition‑none‑regel förlorar på specificitet mot varje regel som har en klass.", "summary": "Inställningen respekteras i praktiken snarare än i avsikt, och elva aktiva animationer som en generell regel såg ut att ha täckt är nu faktiskt täckta.", "content_html": "<p><strong>Situation.</strong> Webbplatsen respekterade inställningen för minskad rörelse med en enda regel som stängde av alla övergångar. Den såg fullständig ut, den passerar lintern utan anmärkning, och under en emulerad inställning för minskad rörelse animerades fortfarande elva selektorer.</p>\n<p><strong>Uppgift.</strong> Inställningen måste respekteras i praktiken, och orsaken till att den uppenbara regeln misslyckades måste göras till något som nästa person inte kan upprepa.</p>\n<p><strong>Handling.</strong> Orsaken är specificitet. En universell regel hamnar längst ned i kaskaden och förlorar mot varje regel som bär en klass, så en generell transition‑none slås ut av varje genomtänkt animation på sidan — vilket är varje animation som är värd att stänga av. Åtgärden var strukturell snarare än en lapp: allt som rör sig ligger nu i en del av stilmallen som kommer sent, och en linter underkänner en transform med övergång var som helst annars. Distinktionen som regeln gör är avsiktlig och snävare än den uppenbara: minska rörelse, inte färg. En färgtoning är inte rörelse, och att stänga av den får gränssnitt att kännas trasiga för människor som inte bad om det, så en övergång som listar färgegenskaper godkänns och en övergång på rörelse gör det inte. Rörelse måste också uttryckas som en övergång snarare än som en keyframe‑animation, eftersom den förra är trivial att avbryta och den senare inte är det. Två närliggande fällor kom ur samma arbete och är dokumenterade med mätningar: ett menyornament hade hunnit trettioen grader in i en vridning på sextio grader innan det var halvt synligt, och eftersom formen är periodisk ser en halv vridning identisk ut för en tredjedel av kostnaden; och fem olika egenskaper gör var för sig ett element till innehållande block för allt som är positionerat relativt visningsområdet, vilket i tysthet hade krympt ett stängningslager till en bråkdel av skärmen.</p>\n<p><strong>Resultat.</strong> Inställningen respekteras i praktiken snarare än i avsikt, och elva aktiva animationer som en generell regel såg ut att ha täckt är nu faktiskt täckta. Den allmänna lärdomen är den som står överst i det dokumentet: en regel som förlorar på specificitet fallerar i tysthet, och varje rapport kallar det något annat.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-637-line-print-stylesheet-136/", "url": "https://engineer.company/sv/portfolio/built-a-637-line-print-stylesheet-136/", "title": "Byggde en utskriftsstilmall på 637 rader mot fyra dokumenterade beteenden i renderingsmotorer, bland annat run‑in‑rubriker som skrevs ut vitt på vitt för varje läsare vars webbläsare föredrog mörkt läge.", "summary": "Webbplatsen skrivs ut på okänt papper i båda temana, och de fyra motorbeteendena är nedskrivna tillsammans med det fel som vart och ett orsakade.", "content_html": "<p><strong>Situation.</strong> Webbplatsens fallstudier är det som någon skriver ut och tar med sig till ett möte. Det gör papper till ett verkligt utdataformat snarare än en artighet, och papper är ett annat medium än en smal skärm — läsarens pappersstorlek är okänd, läsarens sidorientering är okänd, och webbläsarens utskriftsbeteende skiljer sig mellan motorer på sätt som ingen förhandsvisning på skärmen avslöjar.</p>\n<p><strong>Uppgift.</strong> Webbplatsen måste skrivas ut korrekt på okänt papper, i båda färgtemana, utan att ett separat dokument underhålls vid sidan av webbversionen.</p>\n<p><strong>Handling.</strong> Det är en stilmall på 637 rader med en enda sidregel och ett utskriftsblock. Sidregeln anger en marginal och anger avsiktligt ingen pappersstorlek, så det läsaren väljer i dialogrutan går igenom och allt annat anpassar sig efter det. Rotens teckenstorlek är låst i punkter, så att varje relativt mått har ett fysiskt ankare, och en radlängd på ungefär sjuttiotvå tecken tillämpas på de fyra blocken på översta nivån — vilket innebär att arket vinner över designen, eftersom en sida i liggande format annars skulle få rader på ungefär hundratio tecken. Fyra motorbeteenden är dokumenterade som fällor, och vart och ett har orsakat ett verkligt fel. Viewport‑enheter räknas mot sidrutan i en motor och mot fönstret i en annan, så en sida som skrivs ut från ett brett fönster får en tredjedel av varje rad bortskuren i den andra. Element med fast position ritas ut på varje ark, så de dekorativa tas bort och två av dem får en ny uppgift som brevhuvud och kolofon. Paletten har vit text som standard eftersom standardtemat är mörkt, vilket skrev ut de fyra inledande avsnittsetiketterna vitt på vitt — orden saknades helt enkelt. Och paginerade medier kan inte dela en flex- eller grid‑ruta, vilket gav ett tomt första ark på en sida för ett enskilt resultat. En kontroll i sex delar täcker detta och testar sex kombinationer av papper och orientering, båda inställningarna för färgschema och det verkliga sidantalet mot det som innehållets höjd motsvarar.</p>\n<p><strong>Resultat.</strong> Webbplatsen skrivs ut på okänt papper i båda temana, och de fyra motorbeteendena är nedskrivna tillsammans med det fel som vart och ett orsakade. De kända begränsningarna är dokumenterade snarare än dolda: inga sidnummer eller löpande sidhuvuden, en motor ignorerar styrningen av horungar och änkor, och anfanger stöds inte överallt. En vilsekommen kommentarsavslutning fick en gång CSS‑lintern att svälja en hel regel och passera utan anmärkning två gånger — bara utskriftskontrollen märkte det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Designsystem och UI", "Dokumentation", "Frontendutveckling", "UX/UI‑design", "Webbutveckling", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/mirrored-the-site-to-gemini-and-gopher-137/", "url": "https://engineer.company/sv/portfolio/mirrored-the-site-to-gemini-and-gopher-137/", "title": "Speglade hela webbplatsen som 777 Gemini‑dokument och 777 Gopher‑dokument från samma driftsatta träd, utan en enda byte ändrad i HTML‑koden.", "summary": "Webbplatsen går att läsa över fyra protokoll från ett bygge, med 777 dokument vardera och noll ändringar i HTML-koden, och de alternativa representationerna…", "content_html": "<p><strong>Situation.</strong> Webbplatsen är statisk, har ingen spårning och ingen backend vid körning, och dess argument är att ett dokument inte behöver en megabyte JavaScript för att kunna läsas. Det argumentet är lätt att framföra och svårt att visa. Två små internetprotokoll visar det direkt, eftersom inget av dem över huvud taget kan bära ett skript.</p>\n<p><strong>Uppgift.</strong> Hela webbplatsen måste publiceras över Gemini och Gopher från samma innehåll, utan ett andra innehållsträd och utan att HTML‑koden ändrades.</p>\n<p><strong>Handling.</strong> Båda är utdataformat i samma bygge snarare än en separat pipeline. Webbplatsgeneratorn fick egna medietyper och utdataformat, och varje sida, sektion och taxonomiterm fick ytterligare två representationer vid sidan av sin HTML och sin Markdown‑tvilling. Resultatet är 777 Gemini‑dokument och 777 Gopher‑menyer, producerade från samma källa, driftsatta i samma träd och serverade av två små daemoner på samma värd. Två egenskaper gjorde det till något värt att göra snarare än en kuriositet. Båda formaten är markerade som icke‑alternativa representationer, så ingenting i HTML‑koden ändrades — inte en enda byte, och det mättes snarare än antogs. Och Gopher‑formatet saknar möjlighet att placera en länk inuti en mening, eftersom en menyrad består av tabbseparerade fält, så prosan radbryts hårt vid sextioåtta kolumner under bygget; den begränsningen skärpte texten på ett sätt som HTML aldrig krävde. En spegel som onion‑tjänst i Tor finns vid sidan av dem, och den är det enda publika ansikte som plattformen kunde lägga till utan att öppna en port i brandväggen, eftersom daemonen ansluter utåt och ingenting ansluter inåt.</p>\n<p><strong>Resultat.</strong> Webbplatsen går att läsa över fyra protokoll från ett bygge, med 777 dokument vardera och noll ändringar i HTML‑koden, och de alternativa representationerna kostar ungefär tolv procent av HTML‑kodens egen vikt på disk. Bara ett av de fyra mäts för trafik, och det står i plattformens eget statistikdokument i stället för att ignoreras i det tysta.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Frontendutveckling", "Infrastruktur", "Internationalisering", "Webbutveckling", "DevOps och CI/CD‑automatisering", "Internationalisering och lokalisering", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/", "url": "https://engineer.company/sv/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/", "title": "Ersatte 963 anonyma block av strukturerad data som upprepade företaget 1 671 gånger med en enda länkad graf av 16 typer, präglade från stabila ursprungsidentifierare.", "summary": "En graf med stabil identitet ersätter 963 anonyma upprepningar, och sidorna bär mindre i stället för mer.", "content_html": "<p><strong>Situation.</strong> Webbplatsen genererade strukturerade data på samma sätt som de flesta webbplatser: ett block per sida, där varje block beskrev organisationen på nytt från grunden. Över hela webbplatsen blev det 963 block som beskrev samma företag 1 671 gånger, anonymt — ingen stabil identifierare någonstans, så ingenting som läste in dem kunde avgöra att organisationen på en sida var organisationen på en annan.</p>\n<p><strong>Uppgift.</strong> Webbplatsens strukturerade data måste bli en enda graf med stabil identitet, i stället för en hög block som råkar innehålla samma ord.</p>\n<p><strong>Handling.</strong> Identifierare skapas nu utifrån webbplatsens ursprung — en för organisationen, en för webbplatsen, en för personen — och varje block refererar till dem i stället för att återge deras innehåll. Identifierarna gäller för hela ursprunget snarare än per språk, eftersom företaget är samma företag på danska. Sexton typer används, och de täcker tjänstekatalogen och dess erbjudanden, recensionerna och det arbete de beskriver, samlingarna och deras brödsmulor. Två beslut höll nere vikten. Listsidor publicerar sina poster enbart som URL:er i stället för att bädda in dem, och det var uppmätt: att namnge dem på portföljens indexsida skulle ha inneburit 107 fullständiga påståenden och en ökning av den komprimerade sidan med 52 procent. Och kontrollen som vaktar detta validerar inte bara syntax — den säkerställer att varje block går att tolka, att varje identifierare går att slå upp och att varje URL som grafen nämner faktiskt byggdes, så en graf som pekar på en sida som inte finns stoppas i kvalitetsgrinden.</p>\n<p><strong>Resultat.</strong> En graf med stabil identitet ersätter 963 anonyma upprepningar, och sidorna bär mindre i stället för mer. Mätningen som startade arbetet är värd att ha i minnet: webbplatsen hade genererat samma organisationsbeskrivning 1 671 gånger utan att något kunde koppla ihop dem.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Datastyrning", "Frontendutveckling", "Varumärke och marknadsföring", "Webbutveckling", "Datastyrning och datakvalitet", "Redo för AI‑agenter", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/", "url": "https://engineer.company/sv/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/", "title": "Krympte stilmallspaketet från 87 KB till 31 KB genom att ta ut ett base64‑kodat typsnitt ur det, och tog bort 756 KB i 22 filer som publicerades vid varje driftsättning utan att något refererade till dem.", "summary": "Paketet är på 31 KB i stället för 87, 756 KB döda filer driftsätts inte längre, och varje beslut har en mätning kopplad till sig — även det som gick åt andra…", "content_html": "<p><strong>Situation.</strong> Webbplatsens paket med stilmallar var på 87 KB, vilket för en dokumentwebbplats utan ramverk är större delen av en sidvikt som går åt innan något innehåll har kommit fram. Webbplatsen publicerade också en katalog med ikoner vid varje driftsättning, som genererades en gång och som ingenting har refererat till sedan dess.</p>\n<p><strong>Uppgift.</strong> Vikten måste ned utan att designen ändrades, och den måste ned av ett skäl som gick att peka på snarare än genom allmän städning.</p>\n<p><strong>Handling.</strong> Först mätning. Två tredjedelar av den största stilmallen var ett enda base64‑kodat typsnitt — ungefär 56 KB text som kodade ungefär 42 KB typsnitt — inbäddat för en fördel vid första utritningen som en förladdad extern fil ändå får, och betalt på varje sida oavsett om vikten behövdes eller inte. När det togs bort minskade den filen från 84 KB till 28 och hela paketet från 87 till 31. De tre typsnittsvikterna förladdas i stället, och den tredje kom med på listan först när fältdata visade att den avslutade den längsta kritiska kedjan, inte för att tre lät komplett. Därutöver hittade en genomgång av vad driftsättningen faktiskt publicerade tjugoen genererade ikoner och en dubblerad konfigurationsfil, 756 KB, som levererades varje gång och som ingenting refererade till; och webbplatsens egen ikonfil minskade från 145 KB till 15. Ett modernt bildformat utvärderades för varumärkessymbolen, mättes och valdes bort — det blev större, och urvalsmekanismen väljer efter ordning snarare än efter storlek, så en modern webbläsare skulle ha hämtat den tyngre filen överallt. Två kontroller håller nu linjen: inget fotografi får visas bredare än hälften av sina källpixlar, och varje illustration måste publiceras i en storlek som dess egna pixlar räcker till vid både normal och hög pixeltäthet.</p>\n<p><strong>Resultat.</strong> Paketet är på 31 KB i stället för 87, 756 KB döda filer driftsätts inte längre, och varje beslut har en mätning kopplad till sig — även det som gick åt andra hållet, vilket är den mer användbara dokumentationen av de två.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Frontendutveckling", "Prestandaoptimering", "Webbutveckling", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/", "url": "https://engineer.company/sv/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/", "title": "Rättade en sitemap där 172 av 176 URL:er delade samma ändringstidsstämpel genom att hämta datumet ur git‑historiken, efter att ha konstaterat att exporten skriver om varje fil vid varje körning.", "summary": "En ny synkronisering efter en månads innehållsändringar berör nu åtta filer av 186 i stället för samtliga, och webbplatskartan säger något som är sant.", "content_html": "<p><strong>Situation.</strong> Webbplatskartan talade om för sökmotorer att 172 av webbplatsens 176 sidor senast hade ändrats i samma ögonblick. Det är ingen subtil felaktighet — ett ändringsdatum är ett löfte till en sökrobot om att en sidas innehåll har ändrats, och en webbplats som ger det löftet 172 gånger samtidigt säger antingen sanningen om en fullständig omskrivning eller säger ingenting användbart till någon.</p>\n<p><strong>Uppgift.</strong> Datumen måste beskriva innehållet snarare än filen, utan att hitta på en precision som repot inte har.</p>\n<p><strong>Handling.</strong> Orsaken var att datumet hämtades från filens ändringstid och att innehållsexporten skriver om varje fil vid varje körning, så en enda synkronisering stämplade hela korpusen. Åtgärden flyttar källan till versionshistoriken, med en reservkedja som först prövar ett explicit fält, sedan commit‑historiken och sist filen. Fällan i den åtgärden är värd att dokumentera: det bokstavliga fältnamnet måste stå i listan, annars läser generatorn aldrig fältet, så en konfiguration som ser ut att föredra ett datum som författaren har angett, men som utelämnar namnet, ignorerar i tysthet varje sådant datum. Två andra beslut om datum kom ur samma arbete. Databasen anger nu när varje post skrevs och senast reviderades, avsiktligt hållet åtskilt från när arbetet utfördes — det skiljer år mellan dem, och att blanda ihop dem skulle datera en sida som skrevs i år till för ett decennium sedan. Och raderna i den filen är valfria och ingenting härleds, eftersom repots egen historik börjar senare än innehållet: ett datum som saknas lämnar fältet tomt i stället för att registrera en migrering, enligt principen att ett exakt men felaktigt tal är värre än ett som saknas, eftersom det bara är det felaktiga som blir trott.</p>\n<p><strong>Resultat.</strong> En ny synkronisering efter en månads innehållsändringar berör nu åtta filer av 186 i stället för samtliga, och webbplatskartan säger något som är sant. Den allmänna regeln skrevs in i metadatadokumentet vid sidan av åtgärden, eftersom samma fälla gäller för varje fält som har en reservkedja.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Python", "Test och kvalitetssäkring", "Varumärke och marknadsföring", "Webbutveckling", "DevOps och CI/CD‑automatisering", "Redo för AI‑agenter", "Varumärke, marknadsföring och SEO", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/ran-development-as-49-written-initiatives-141/", "url": "https://engineer.company/sv/portfolio/ran-development-as-49-written-initiatives-141/", "title": "Drev webbplatsens utveckling som 51 skriftliga initiativ i 54 plandokument, vart och ett med sina öppna rutor, sina mätningar och de beslut det avböjde — bland dem sex av åtta fynd om agentberedskap som avvisades med skälet nedskrivet.", "summary": "Femtioett beslut med motiveringen bifogad, ungefär tre fjärdedelar avslutade, och de bortvalda går lika lätt att hitta som de godtagna.", "content_html": "<p><strong>Situation.</strong> Ett enmansföretag har inget planeringsmöte, ingen genomgång av backloggen och ingen som kan säga emot ett beslut. Vad det har i stället är en stark tendens att göra det arbete som för tillfället är intressant, och ingen dokumentation i efterhand av varför något valdes — vilket blir ett problem första gången ett beslut behöver omprövas eller försvaras.</p>\n<p><strong>Uppgift.</strong> Utvecklingen av webbplatsen behövde en skriftlig dokumentation per initiativ som håller för att läsas om månader senare, och som omfattar de initiativ som valdes bort lika väl som de som byggdes.</p>\n<p><strong>Handling.</strong> Femtioett initiativ fördelade på femtiofyra numrerade och indexerade plandokument, där vart och ett innehåller problemet, de mätningar som gjordes, beslutet och dess öppna kryssrutor. De är inte sammanfattningar som skrevs i efterhand. De innehåller siffrorna som avgjorde frågan — schemaläggningsmätningarna som formade verktyget som kör kontrollerna, bildformatet som testades och valdes bort eftersom det blev större, pristrappan som ägaren bekräftade, genomgången som visade att webbplatsen påvisade kompetens över 371 sidor utan att någonstans ange något pris, någon uppdragsform eller något minimum. Bortvalda fynd behandlas på samma sätt som godtagna: en granskning av agentberedskapen gav åtta fynd och sex avvisades, vart och ett med utslaget registrerat, uttryckligen för att ett senare automatiskt förslag inte i det tysta ska kunna upphäva ett skriftligt beslut. Statustavlan läser en plans kryssrutor snarare än dess prosa, eftersom prosan säger ”klart” och kryssrutorna säger vad som är öppet. Den distinktionen var inte teoretisk — en linter för dokumentationen hittade senare sex planer som var markerade som klara men fortfarande hade okryssade rutor.</p>\n<p><strong>Resultat.</strong> Femtioett beslut med motiveringen bifogad, ungefär tre fjärdedelar avslutade, och de bortvalda går lika lätt att hitta som de godtagna. Priset är att varje initiativ får en skriftlig redogörelse, vilket är en verklig belastning på små arbeten och ibland har inneburit att redogörelsen är längre än ändringen den beskriver.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Dokumentation", "Intressenter och rapportering", "Produkt och krav", "Projektledning", "Tekniskt ledarskap", "Produktstrategi och krav", "Projektledning (agil)", "Redo för AI‑agenter", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/put-the-documentation-under-its-own-linter-142/", "url": "https://engineer.company/sv/portfolio/put-the-documentation-under-its-own-linter-142/", "title": "Satte dokumentationen under en egen linter — en radbudget per fil som bara kan skruvas nedåt, ett krav på index och en kontroll av verkliga sökvägar — som hittade sex av nio planer markerade som klara trots okryssade rutor, och fjorton commit‑spärrande uppgifter som inget dokument nämnde.", "summary": "Tjugosex dokument på sammanlagt ungefär 2 666 effektiva rader, vart och ett under en budget som inte kan växa, och varje sökväg och symbol som de nämner…", "content_html": "<p><strong>Situation.</strong> Repot styr sig självt med skrivna instruktioner — tjugosex dokument som täcker kvalitetspolicyn, designsystemet, tillgänglighet, utskrift, metadata, översättning och git. Instruktioner har ett specifikt förfallsmönster: de blir längre, de glider ifrån koden, de börjar hänvisa till filer som har flyttats, och vid en viss storlek slutar de att läsas. En regel som ingen läser är ingen regel.</p>\n<p><strong>Uppgift.</strong> Dokumentationen behövde samma behandling som koden — en linter med gränser som fallerar i stället för att ge råd.</p>\n<p><strong>Handling.</strong> Åtta policyer, var och en mekanisk. Ett hårt tak för dokumentlängd och ett mjukare som varnar. En radbudget per fil som bara kan sänkas, så att ett dokument kan krympa men inte växa tillbaka. Ett krav på innehållsförteckning över ett tröskelvärde, och en avsnittsförteckning över ett lägre. En placeringsregel om vilket dokument som äger vilket ämne. Giltiga länkar. Ärlighet i planerna — den status en plan anger måste stämma med dess egna kryssrutor. En regel om att varje uppgift som spärrar en commit är namngiven i något dokument. Och en kontroll av verkliga sökvägar, att varje katalogkvalificerad sökväg och varje kodsymbol som ett dokument nämner faktiskt finns. Den första körningen var argumentet för hela övningen: sex planer som var markerade som klara men fortfarande hade okryssade rutor, fjorton uppgifter som spärrade varje commit och som inte nämndes i något dokument alls, tre trasiga länkar, ett föräldralöst dokument och tre döda sökvägar i fem dokument. Budgeten stoppade sedan sin egen upphovsperson — när sammanfattningen av detta arbete skrevs in i utvecklingsdokumentet gick det över sin gräns, och det var så ett separat dokument kom till, vilket är policyn som fungerar precis som avsett.</p>\n<p><strong>Resultat.</strong> Tjugosex dokument på sammanlagt ungefär 2 666 effektiva rader, vart och ett under en budget som inte kan växa, och varje sökväg och symbol som de nämner kontrolleras mot trädet. Det obekväma fyndet är det som är värt att upprepa: fjorton kontroller spärrade varenda commit utan att förekomma i något dokument, så reglerna som maskinen upprätthöll och reglerna som människorna läste hade redan glidit isär.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Projektledning", "Python", "Tekniskt ledarskap", "Test och kvalitetssäkring", "Projektledning (agil)", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/brought-the-quality-gate-code-under-a-linter-143/", "url": "https://engineer.company/sv/portfolio/brought-the-quality-gate-code-under-a-linter-143/", "title": "Förde in 10 242 rader JavaScript för kvalitetsgrinden under en formaterare och en linter efter att ha konstaterat att det var den största kodmassan i repot och den enda som inget läste, och åtgärdade 13 fynd utan att undertrycka något.", "summary": "Den största och mest bärande koden i repot är nu formaterad, lintad och typkontrollerad, med tretton fynd åtgärdade och noll undertryckningar.", "content_html": "<p><strong>Situation.</strong> Webbplatsens kvalitetsgrind består av trettiotvå skript på sammanlagt 10 242 rader JavaScript. Den var med bred marginal den största mängden kod i repot, och den enda kod som ingenting läste — ingen formaterare, ingen linter, ingen typkontroll. Programmen som upprätthöll varje regel i projektet var de enda program som inte omfattades av någon av dem.</p>\n<p><strong>Uppgift.</strong> Kontrollprogrammen måste hållas till den standard som de finns till för att upprätthålla, och formateraren måste anpassas efter dem snarare än tvärtom.</p>\n<p><strong>Handling.</strong> Formateraren kom först, och indenteringsbredden var det intressanta beslutet. Repots standard är fyra mellanslag; med fyra mellanslag skulle formateraren ha skrivit om 2 173 rader i det största kontrollprogrammet. En åsidosättning till två mellanslag för de filerna minskade det till 38 rader verklig avvikelse. Principen som dokumenterades tillsammans med beslutet är att formateraren anpassas efter koden, inte koden efter formateraren — att formatera om tvåtusen rader för att tillfredsställa en preferens förstör möjligheten att läsa filens historik. Sedan lintern, som gav tretton verkliga fynd, alla åtgärdade och inget undertryckt. Python‑kontrollprogrammen fick samma behandling genom en typkontroll som konfigurerades på en mellannivå av strikthet med trettio enskilda regler från den strikta nivån aktiverade ovanpå, valda genom mätning: med den inställningen är trädet tyst, och av de trettio kandidaterna var tjugonio redan tysta och en gav utslag — och den åtgärdades i stället för att undantas. Typkontrollen hittade två verkliga fel som lintern hade släppt igenom utan anmärkning, båda om ett värdes form snarare än dess syntax.</p>\n<p><strong>Resultat.</strong> Den största och mest bärande koden i repot är nu formaterad, lintad och typkontrollerad, med tretton fynd åtgärdade och noll undertryckningar. Skälet till att det betyder mer än radantalet antyder står i planen: ett fel i webbplatsens stilmall visar sig som en sida som ser fel ut, och ett fel i ett kontrollprogram visar sig som en kontroll som går igenom när den inte borde.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Frontendutveckling", "Python", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-database-driven-cv-and-portfolio-generator-144/", "url": "https://engineer.company/sv/portfolio/built-a-database-driven-cv-and-portfolio-generator-144/", "title": "Byggde en databasdriven generator för CV, referenser, portfolio och personliga brev i Python — 41 moduler, 10 580 rader — som renderar sex utdataformat från en SQLite‑källa med 23 tabeller, sammanställd av en idempotent pipeline i 19 steg.", "summary": "Sex format, tre språk och fem teman kommer ur en enda databas, och en rättad mening rättas en gång.", "content_html": "<p><strong>Situation.</strong> Ett CV, ett referensblad, en portfölj och ett personligt brev är samma fakta ordnade på fyra sätt, och att hålla dem som fyra dokument innebär att varje rättelse görs fyra gånger och till slut inte görs alls. Tre språk multiplicerar det med tre. Problemet är inte att ett dokument är fel; det är att två dokument säger emot varandra och att ingenting säger vilket som är aktuellt.</p>\n<p><strong>Uppgift.</strong> En enda faktakälla måste producera varje dokument, på varje språk och i varje format, där ordningen bestäms av kod snarare än av den som senast redigerade en fil.</p>\n<p><strong>Handling.</strong> Källan är en SQLite‑databas med tjugotre tabeller — resultat, titlar, företag, regioner, kategorier, tjänster, profiler, referenser, sammanfattningar — som byggs upp av en pipeline i nitton steg som går från en tom fil till en komplett databas med ett enda kommando. Stegen är ordnade och vart och ett är skrivet så att det kan upprepas säkert, så att pipelinen kan köras mot en befintlig databas utan att någon rad dubbleras. Fyrtioen Python‑moduler på sammanlagt 10 580 rader renderar sex utdataformat från den: HTML, PDF i två varianter, ren text, Markdown och JSON, genom sex Jinja‑mallar och åtta stilmallar. Beroendena vid körning hölls till exakt två, en mallmotor och en PDF‑renderare, med argumentet att en dokumentgenerator som inte går att installera om fem år inte har bevarat någonting. Målgruppsanpassning är ett fullvärdigt begrepp i systemet snarare än en manuell redigering: en profil väljer vilka resultat som visas och i vilken ordning, så ett dokument riktat till en viss sorts läsare är en databasfråga snarare än en kopia.</p>\n<p><strong>Resultat.</strong> Sex format, tre språk och fem teman kommer ur en enda databas, och en rättad mening rättas en gång. Kostnaden är att systemet nu är det enda sättet att ta fram ett dokument — det finns inte längre någon fil att öppna och redigera i all hast, och att lägga till ett språk innebär att lägga till det överallt innan något alls går att bygga.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Backendutveckling", "Databaser", "Fullstackutveckling", "Internationalisering", "Lösningsarkitektur", "Python", "SQL", "Backend- och API‑utveckling", "Databasdesign och datamodellering", "Fullstackutveckling av produkter", "Internationalisering och lokalisering", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/", "url": "https://engineer.company/sv/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/", "title": "Krävde 981 testfall och minst 92 % grentäckning av generatorn, med varningar behandlade som fel, och säkerställde idempotens genom att köra hela byggpipelinen två gånger från en tom fil och kräva att den andra körningen inte ändrade något.", "summary": "Pipelinen kan köras om mot en databas i drift utan oro, vilket är det som över huvud taget gör inkrementellt innehållsarbete möjligt.", "content_html": "<p><strong>Situation.</strong> En generator som bygger upp en databas från grunden har en särskild sorts bugg: den fungerar första gången och korrumperar andra gången. Steg som infogar utan att kontrollera, steg som är beroende av ordningen i ett tidigare stegs utdata, steg som är säkra var för sig men inte tillsammans. Inget av det syns i ett test som börjar från ingenting och körs en gång.</p>\n<p><strong>Uppgift.</strong> Bygget måste bevisas vara upprepningsbart snarare än bara fungerande, och testsviten måste vara tillräckligt stor och tillräckligt strikt för att en regression inte skulle kunna passera obemärkt.</p>\n<p><strong>Handling.</strong> Idempotenstestet är det trubbiga och det mest användbara: hela databasen byggs från en tom fil, en ögonblicksbild tas, hela pipelinen i nitton steg körs igen över resultatet, och den andra körningen får inte ändra någonting. Radantal, innehåll och identifierare måste alla stämma överens. Runt det finns 383 testfunktioner — 981 fall när de parametriserade har expanderats — fördelade på fyrtiofem filer och 7 944 rader, som täcker pipelinen, renderarna, innehållsinläsarna, logiken för målgruppsanpassning och kontrollprogrammen. Grentäckningen har ett golv på nittiotvå procent som upprätthålls i bygget snarare än rapporteras i en sammanfattning, och sviten ligger för närvarande på ungefär 95 procent, så golvet har marginal utan att vara dekorativt. Varningar är konfigurerade som fel, vilket är den inställning som betyder mest i praktiken — ett meddelande om utfasning som skrivs ut i två år är ett meddelande som ingen läser, och den körning som gör det till ett rött test är den körning som får det åtgärdat.</p>\n<p><strong>Resultat.</strong> Pipelinen kan köras om mot en databas i drift utan oro, vilket är det som över huvud taget gör inkrementellt innehållsarbete möjligt. Golvet är ett golv, inte ett mål, och det är värt att säga att nittiotvå procents grentäckning fortfarande lämnar grenar som ingenting någonsin har gått igenom — talet begränsar risken men tar inte bort den, och två av de fel som hittades senare i projektet låg i kod som täckningsrapporten visade som täckt.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Databaser", "DevOps", "Python", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/", "url": "https://engineer.company/sv/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/", "title": "Etablerade PDF/UA‑1‑överensstämmelse i nio dokument med 106 av 106 regler, och upptäckte att arkivvarianten föll på en regel av 146 — ett knappt underkännande som ser ut som godkänt för den som inte kör valideraren.", "summary": "Överensstämmelsen med tillgänglighetsstandarden är bevisad med full poäng, och arkivanspråket redovisas ärligt som nästan uppfyllt, med den specifika luckan…", "content_html": "<p><strong>Situation.</strong> Att en PDF ser korrekt ut på skärmen säger ingenting om huruvida en skärmläsare kan läsa den, om dess rubriker bildar en struktur, om dess språk är deklarerat eller om den fortfarande går att öppna om tjugo år. Överensstämmelse med standarderna för tillgänglig PDF och för arkiv‑PDF kan båda kontrolleras maskinellt, och ett dokument som aldrig har körts genom valideringsverktyget är ett dokument som gör ett overifierat påstående.</p>\n<p><strong>Uppgift.</strong> De genererade dokumenten måste mätas mot båda standarderna, med resultatet registrerat som ett tal snarare än som en avsikt.</p>\n<p><strong>Handling.</strong> Nio dokument — CV:t i sina varianter, referensbladet, portföljen och det personliga brevet, på flera språk — kördes genom ett oberoende valideringsverktyg för tillgänglighetsprofilen och arkivprofilen var för sig. Tillgänglighetsprofilen godkänns på 106 av 106 regler, vilket krävde taggad struktur, deklarerat dokumentspråk, alternativtext på varje grafik som inte är dekorativ, en riktig titel i metadata och en explicit läsordning i stället för den som layouten råkar ge. Arkivprofilen är det mer intressanta resultatet: den fallerar på exakt en regel av 146. Det är just den typ av resultat som är lätt att rapportera fel. Hundrafyrtiofem godkända regler ser ut som överensstämmelse i en sammanfattning, och det är inte överensstämmelse; det är ett dokument som kommer att avvisas av ett system som upprätthåller standarden. Regeln som fallerar är dokumenterad med vad den är och varför den inte har åtgärdats, i stället för att avrundas bort.</p>\n<p><strong>Resultat.</strong> Överensstämmelsen med tillgänglighetsstandarden är bevisad med full poäng, och arkivanspråket redovisas ärligt som nästan uppfyllt, med den specifika luckan namngiven. Begränsningen som är värd att vara rak om är att båda talen kommer från ett och samma valideringsverktyg; en annan implementation kan komma fram till något annat, och en regel som godkänns är bara belägg för att just det här kontrollprogrammet inte hade något att anmärka på den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Python", "Test och kvalitetssäkring", "UX/UI‑design", "Webbutveckling", "Teknisk dokumentation", "UI/UX‑design och designsystem", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/portfolio/enabled-every-python-linter-rule-as-an-error-147/", "url": "https://engineer.company/sv/portfolio/enabled-every-python-linter-rule-as-an-error-147/", "title": "Aktiverade varje regel i Python‑lintern på felnivå och arbetade igenom 1 815 fynd ned till noll, där vart och ett av de få undantagen har ett skriftligt skäl och två av dem backas upp av en kontroll i stället för en kommentar.", "summary": "Lintern körs med full styrka och noll fynd, och varje avvikelse är dokumenterad på den rad där den görs.", "content_html": "<p><strong>Situation.</strong> De flesta projekt väljer en bekväm delmängd av sin linters regler, och delmängden väljs efter vilka regler som råkade vara tysta den dag konfigurationen gjordes. Det gör konfigurationen till en förteckning över kodens befintliga vanor snarare än en standard som koden hålls till, och varje regel som lämnas avstängd är en felklass som ingen någonsin kommer att få höra talas om.</p>\n<p><strong>Uppgift.</strong> Standardläget måste vändas — varje regel som verktyget implementerar aktiverad som fel — och den eftersläpning som blev följden måste arbetas ned till noll snarare än förhandlas ned genom att regler stängs av igen.</p>\n<p><strong>Handling.</strong> När hela regeluppsättningen valdes gav den 1 815 fynd vid första körningen. De arbetades igenom kategori för kategori snarare än fil för fil, eftersom kategorierna säger något: oanvända argument och skuggade inbyggda namn är brus, men säkerhetskategorin, kategorin för muterbara standardvärden och kategorin för undantagshantering pekade var och en på verkligt beteende. Verkliga inkompatibiliteter finns — en formaterare och en linter kan vara oense om samma rad, och några regler motsäger projektets egna avsiktliga val — och vart och ett av det fåtal undantag som finns har en skriftlig motivering på det ställe där undantaget görs, som säger vad regeln ville och varför den här koden gör på ett annat sätt. Två av dem går längre och backas upp av en kontroll snarare än en kommentar, så att undantaget inte i det tysta kan vidgas: regeln är avstängd, och ett test säkerställer just den egenskap som regeln skulle ha upprätthållit. Typkontroll körs i strikt läge vid sidan av, vilket är en separat och svårare standard, och det är den som fångade fel som lintern inte kunde se, eftersom de handlar om vad ett värde är snarare än om hur det är skrivet.</p>\n<p><strong>Resultat.</strong> Lintern körs med full styrka och noll fynd, och varje avvikelse är dokumenterad på den rad där den görs. Kostnaden är verklig och värd att nämna: den striktaste inställningen ger fynd som verkligen inte är värda att åtgärda, och någon måste göra den bedömningen 1 815 gånger i stället för en gång.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Python", "Test och kvalitetssäkring", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/vendored-a-qr-encoder-in-522-lines-148/", "url": "https://engineer.company/sv/portfolio/vendored-a-qr-encoder-in-522-lines-148/", "title": "Byggde in en QR‑kodare — Reed‑Solomon över GF(256), fast modullayout, åtta maskmönster — på 522 rader i stället för att ta in ett tredje körtidsberoende, och verifierade den genom att läsa tillbaka den färdiga matrisen med en oberoende skriven avkodare.", "summary": "Antalet beroenden vid körning stannade på två, och kodaren är den enda komponenten som projektet bär med sig i sitt eget källträd.", "content_html": "<p><strong>Situation.</strong> Dokumenten behövde en QR‑kod som länkade till onlineversionen. Varje tillgängligt bibliotek klarar det, och att ta ett av dem hade lagt till ett tredje beroende vid körning i ett projekt som avsiktligt hade hållit sig till två — en mallmotor och en PDF‑renderare — med argumentet att en dokumentgenerator fortfarande ska gå att installera om flera år.</p>\n<p><strong>Uppgift.</strong> Antingen måste beroendet godtas eller formatet implementeras, och implementationen måste verifieras som korrekt snarare än att bara producera något fyrkantigt och svart.</p>\n<p><strong>Handling.</strong> Kodaren är på 522 rader och implementerar de delar av specifikationen som användningen faktiskt kräver: kodning i byteläge, felkorrigering med Reed‑Solomon över Galoiskroppen med 256 element, där generatorpolynomet byggs med den grad som krävs, den fasta modullayouten med dess positionsmönster, tidsmönster och justeringsmönster, format- och versionsinformation, samt alla åtta datamaskmönster, som utvärderas mot specifikationens fyra regler för straffpoäng så att masken med lägst poäng väljs i stället för en fast mask. Verifieringen är den del som gjorde det försvarbart. Att testa en kodare mot dess egen logik bevisar ingenting, så den färdiga modulmatrisen läses tillbaka av en avkodare som har skrivits oberoende utifrån specifikationens läsordning, och testet säkerställer att den avkodade strängen är lika med indata. Det gör ”den producerar en rimlig bild” till ”den producerar en kod som avkodas till rätt URL”, och det kontrolleras vid varje körning i stället för en gång med blotta ögat och en telefon.</p>\n<p><strong>Resultat.</strong> Antalet beroenden vid körning stannade på två, och kodaren är den enda komponenten som projektet bär med sig i sitt eget källträd. Det ska sägas rakt ut att detta inte är en allmän implementation — den stöder de lägen och versioner som dokumenten använder och ingenting annat, och den ärliga motiveringen är beroendebudgeten snarare än något påstående om att resultatet är bättre än ett moget bibliotek.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Dokumentation", "Prestandaoptimering", "Python", "Test och kvalitetssäkring", "Backend- och API‑utveckling", "Plattforms- och lösningsarkitektur" ] }, { "id": "https://engineer.company/sv/portfolio/wrote-tests-for-the-checkers-themselves-149/", "url": "https://engineer.company/sv/portfolio/wrote-tests-for-the-checkers-themselves-149/", "title": "Skrev tester för själva kontrollerna efter att ha konstaterat att en kontroll som bara matas med felfri indata en dag rapporterar felfritt för att den inte läste något — lade in en avsiktlig felstavning för att bekräfta att stavningskontrollen hittar den och hämtade ett id‑intervall från databasen i stället för från ett tal i testet.", "summary": "Varje kontrollprogram i projektet har nu ett test som bevisar att det fallerar på felaktig indata, och intervallkontrollerna läser från sanningskällan.", "content_html": "<p><strong>Situation.</strong> Projektet kör en uppsättning egna kontrollprogram över sitt eget innehåll — en stavningskontroll, en kontroll av identifierarintervall, en kontroll av berättarrösten, en kontroll av sifferkonsistens. Vart och ett av dem hade varit grönt i månader. Ett kontrollprogram som bara någonsin har sett felfri indata och bara någonsin har rapporterat felfritt går inte att skilja från ett kontrollprogram som inte läser någonting alls, och det fanns inget test i sviten som kunde skilja de två åt.</p>\n<p><strong>Uppgift.</strong> Kontrollprogrammen måste fås att bevisa att de kan fallera, och fixturerna som de kontrollerar mot måste sluta vara handunderhållna kopior av det som de beskriver.</p>\n<p><strong>Handling.</strong> Mönstret som tillämpas genomgående är att plantera det fel som kontrollprogrammet finns till för att hitta och kräva att kontrollprogrammet hittar det. Stavningskontrollen matas med ett avsiktligt felstavat ord, och testet fallerar om körningen kommer tillbaka felfri. Kontrollen av berättarrösten matas med prosa i första person och måste avvisa den. Kontrollen av modeord matas med ett förbjudet ord. Vart och ett av dessa är ett litet test, och vart och ett täppte till en verklig blind fläck, eftersom två av kontrollprogrammen visade sig läsa en snävare uppsättning filer än deras dokumentation påstod och i tysthet hade hoppat över innehåll. Den andra ändringen gäller varifrån ett test får sina förväntningar: kontrollen av identifierarintervall jämförde tidigare mot ett tal som stod skrivet i testfilen, vilket innebar att varje tillägg av innehåll krävde att ett test redigerades, och den som uppdaterade talet utan att titta hade satt kontrollen ur spel. Nu härleder den intervallet från databasen, så kontrollen beskriver data snarare än ett inaktuellt minne av dem.</p>\n<p><strong>Resultat.</strong> Varje kontrollprogram i projektet har nu ett test som bevisar att det fallerar på felaktig indata, och intervallkontrollerna läser från sanningskällan. Den obekväma delen är vad detta blottlade: en grön kontroll hade varit meningslös på minst två ställen under en okänd tid, och det finns inget sätt att i efterhand ta reda på vad som slank igenom.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Databaser", "Datastyrning", "Python", "Test och kvalitetssäkring", "Datastyrning och datakvalitet", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/", "url": "https://engineer.company/sv/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/", "title": "Upptäckte att commit‑hookarna och kvalitetsgrinden körde olika kontroller medan ett dokument lovade att de var desamma, genom att jämföra de två listorna i ett test — de fem som bara någonsin kördes för hand var just de som läste CV‑texten.", "summary": "Hooken och grinden kör bevisligen samma kontroller, och prosakontrollerna körs nu vid varje commit.", "content_html": "<p><strong>Situation.</strong> Projektet hade en commit‑hook som kör kontroller innan en commit godtas, och en fullständig kvalitetsgrind som körs på begäran. Ett dokument påstod att hooken kör grinden, så att ingenting kunde nå historiken utan att ha klarat allt. Båda listorna underhölls för hand, i två olika filer, och ingenting jämförde dem.</p>\n<p><strong>Uppgift.</strong> Påståendet måste göras om till något som kontrolleras maskinellt, vilket innebar att båda mängderna räknades upp programmatiskt och att testet fallerade när de skilde sig åt.</p>\n<p><strong>Handling.</strong> Testet läser hook‑konfigurationen och grindens task‑definitioner, löser upp var och en till den mängd kontroller som den faktiskt anropar och jämför dem. De stämde inte överens. Fem kontroller fanns bara i grinden och kördes aldrig vid commit, och de fem var inte slumpmässigt valda — det var de som läser själva CV‑prosan: kontrollen av berättarrösten som håller genomgångarna opersonliga, kontrollen av modeord, kontrollen av sifferkonsistens mellan språken, notationskontrollen och stavningskontrollen av innehållet. Med andra ord kördes varje kontroll som skyddade koden automatiskt, och varje kontroll som skyddade texten kördes bara när någon kom ihåg det. Eftersom texten är hela produkten var exponeringen den omvända mot vad vem som helst skulle ha gissat. Åtgärden var att föra in de fem i hooken, vilket krävde att två av dem gjordes tillräckligt snabba för att klara en tidsbudget före commit, och sedan att behålla jämförelsetestet så att de två listorna inte kan glida isär igen. Dokumentet som hade beskrivit en avsikt beskriver nu något som upprätthålls.</p>\n<p><strong>Resultat.</strong> Hooken och grinden kör bevisligen samma kontroller, och prosakontrollerna körs nu vid varje commit. Avvägningen är commit‑hookens körtid, som har ökat och kommer att fortsätta öka i takt med att innehållet växer, och det finns en punkt där en långsam hook kringgås — så den här åtgärden har en hållbarhetstid som mäts i hur länge kontrollerna förblir snabba.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Dokumentation", "Python", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/rehearsed-the-forge-side-ci-hook-151/", "url": "https://engineer.company/sv/portfolio/rehearsed-the-forge-side-ci-hook-151/", "title": "Repeterade CI‑hooken på forge‑sidan och hittade två fel som inte gick att se genom att läsa filen: en reservväg som lade ett argument som inte gick att lösa upp i hookens indata, och en miljövariabel från git själv som följde med grinden in i utcheckningen och gjorde 19 tester röda.", "summary": "Hooken fungerar i båda fallen, och båda felen hittades innan någon riktig push stötte på dem. Begränsningen är att en provkörning fortfarande är en simulering:…", "content_html": "<p><strong>Situation.</strong> Den egenhostade git‑plattformen tar emot pushar och kan köra en hook på serversidan som avvisar arbete som inte klarar grinden. En hook på serversidan är den enda del av automatiseringen som inte går att testa genom att köra den lokalt: den körs i ett bare‑repo utan arbetskatalog, i en miljö som plattformen sätter upp, och läser de pushade referenserna från standard in. Att läsa skriptet och dra slutsatsen att det är korrekt är en gissning.</p>\n<p><strong>Uppgift.</strong> Hooken måste provköras mot ett riktigt bare‑repo och en riktig push innan den kunde betraktas som pålitlig, i stället för att driftsättas och visa sina brister först i skarp drift.</p>\n<p><strong>Handling.</strong> Provkörningen skapar ett bare‑repo, installerar hooken, klonar repot, committar, pushar och verifierar både fallet där pushen godtas och fallet där den avvisas. Den hittade två fel, och inget av dem syntes i filen. Det första var en reservlösning i argumenthanteringen: när hooken inte kunde avgöra en referens satte den in en platshållare som inte gick att slå upp som objekt, och eftersom värdet kommer in på hookens standard in och inte som en parameter, visade sig felet som ett helt annat fel mycket längre ned. Det andra är det som är värt att komma ihåg. Git exporterar en miljövariabel som pekar ut repots katalog när det anropar en hook, och den variabeln ärvs av allt som hooken kör. Grinden checkar ut den pushade revisionen och kör testsviten i den, och testsviten ärvde pekaren till bare‑repot — så nitton tester som rör git slogs upp mot fel repo och blev röda, trots att koden var korrekt. Miljön måste rensas vid gränsen, och den gränsen syns inte förrän saken faktiskt körs.</p>\n<p><strong>Resultat.</strong> Hooken fungerar i båda fallen, och båda felen hittades innan någon riktig push stötte på dem. Begränsningen är att en provkörning fortfarande är en simulering: den bevisar att hooken klarar en sorts push, och plattformen i produktion tar emot pushar som den här provkörningen inte konstruerar, bland annat tvingade uppdateringar och borttagna grenar.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Infrastruktur", "Linux och servrar", "Python", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Infrastruktur som kod", "Systemadministration" ] }, { "id": "https://engineer.company/sv/portfolio/moved-every-user-facing-string-into-a-content-tree-152/", "url": "https://engineer.company/sv/portfolio/moved-every-user-facing-string-into-a-content-tree-152/", "title": "Flyttade varje sträng som användarna ser ut ur Python till ett innehållsträd med 874 filer på tre språk, efter att ha hittat döda översättningar som ingen kunde se var döda och en kontroll som i tysthet bedömde en tredjedel av resultaten.", "summary": "Texterna går nu att redigera utan att röra koden och att jämföra mellan språk genom att räkna. Priset är att ett nytt språk nu läggs till i ett entydigt steg i…", "content_html": "<p><strong>Situation.</strong> Varje sträng som användaren ser i generatorn — varje resultat, varje genomgång, varje avsnittsrubrik, varje sammanfattning, på tre språk — låg i Python‑källkoden. Det gör en ändrad mening till en kodändring, gör granskningen av en översättning till en diff mot källkoden och gör det omöjligt att med en blick avgöra om ett språk är komplett. Det döljer också det felläge som spelar roll: en översättning kan på samma gång finnas, vara fel och vara onåbar.</p>\n<p><strong>Uppgift.</strong> Strängarna måste flyttas ut ur koden till ett innehållsträd som går att räkna, jämföra mellan språk och kontrollera utan att köra renderaren.</p>\n<p><strong>Handling.</strong> Det blev 874 filer under en innehållskatalog, ordnade först efter typ och sedan efter språk: resultat, genomgångar, sammanfattningar, titlar, företag, regioner, kategorier, tjänster, profiler, referenser och fragment av personliga brev. Tabbseparerade filer där enheten är en rad med en identifierare, enskilda filer där enheten är ett stycke. Inläsaren läser dem vid byggtid och databasen sätts samman av dem, så trädet är källan och databasen härleds ur det. Två fynd kom direkt ur möjligheten att räkna. Vissa översatta strängar saknade numera engelsk motsvarighet — döda poster som ingenting renderade och som ingen kunde ha märkt så länge de låg inbäddade i koden, eftersom en ordboksnyckel som ingenting refererar till ser exakt likadan ut som en som används. Och en innehållskontroll som skulle bedöma varje resultat läste bara den delmängd den kunde slå upp, bedömde trettiosju av nittiotre och rapporterade framgång. När korpusen blev en katalog syntes båda som en avvikelse i antalet filer.</p>\n<p><strong>Resultat.</strong> Texterna går nu att redigera utan att röra koden och att jämföra mellan språk genom att räkna. Priset är att ett nytt språk nu läggs till i ett entydigt steg i stället för gradvis — trädet gör ett ofullständigt språk uppenbart, vilket är poängen, men det betyder också att halvfärdiga översättningar inte kan levereras i tysthet medan de färdigställs.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Datastyrning", "Dokumentation", "Internationalisering", "Migrering och modernisering", "Python", "Backend- och API‑utveckling", "Databasmigrering och modernisering", "Datastyrning och datakvalitet", "Internationalisering och lokalisering" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-cross-language-figure-and-notation-check-153/", "url": "https://engineer.company/sv/portfolio/built-a-cross-language-figure-and-notation-check-153/", "title": "Byggde en innehållskontroll mellan språken som fäller när en översättning tappar en siffra som den engelska texten anger, och när ett språk använder en notation som det inte använder — och hittade två danska beskrivningar som saknade ett mätvärde och sexton ukrainska textavsnitt som citerade på engelskt sätt.", "summary": "Arton verkliga fel åtgärdades, och hela felklassen stängdes med dem, eftersom varje framtida översättning jämförs med sin engelska källa innan den kan…", "content_html": "<p><strong>Situation.</strong> Den mest skadliga sortens översättningsfel i ett cv är inte en klumpig formulering. Det är en siffra som försvinner. En engelsk mening som anger en femtiofaldig förbättring och som översätts till en mening som säger ”avsevärt” är ett påstående som i tysthet dras tillbaka på en marknad och står kvar på en annan — och ingen stavningskontroll, grammatikkontroll eller mänsklig läsning av enbart målspråket kommer någonsin att märka det, eftersom den översatta meningen är fullgod prosa.</p>\n<p><strong>Uppgift.</strong> Det behövdes en kontroll som läser språken mot varandra i stället för vart och ett för sig, i fråga om de två saker som måste överleva översättningen: siffrorna och den notation som varje språk använder för att skriva dem.</p>\n<p><strong>Handling.</strong> Sifferkontrollen plockar ut varje tal, procentsats, multiplikator och enhet ur den engelska strängen och kräver att vart och ett finns med i varje översättning av strängen, där multiplikatorformerna mappas per språk i stället för att matchas bokstavligt — den engelska formen för femtio gånger motsvarar en bestämd dansk formulering och en bestämd ukrainsk formulering, och kontrollen känner till mappningen i stället för att kräva enbart siffrorna. Notationskontrollen är dess spegelbild: varje språk har konventioner det måste använda och konventioner det inte får använda, bland annat decimaltecken, tusentalsgruppering och citattecken. Ukrainskan använder anföringsvinklar; engelska dubbla citattecken i en ukrainsk mening är lika fel som en saknad siffra, och betydligt lättare att råka föra in genom att kopiera. Den första fullständiga körningen hittade två danska beskrivningar där ett mätvärde som fanns i engelskan hade fallit bort, och sexton ukrainska textavsnitt med citattecken i engelsk stil.</p>\n<p><strong>Resultat.</strong> Arton verkliga fel åtgärdades, och hela felklassen stängdes med dem, eftersom varje framtida översättning jämförs med sin engelska källa innan den kan committas. Det kontrollen inte kan göra är att bedöma betydelse: den bevisar att talet överlevde och att interpunktionen är språkets egen, och en översättning som behåller varje siffra men vänder påståendet bakfram passerar utan anmärkning.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Datastyrning", "Dokumentation", "Internationalisering", "Python", "Test och kvalitetssäkring", "Datastyrning och datakvalitet", "Internationalisering och lokalisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-native-macos-messaging-client-in-swift-6-154/", "url": "https://engineer.company/sv/portfolio/built-a-native-macos-messaging-client-in-swift-6-154/", "title": "Byggde en native meddelandeklient för macOS i Swift 6 och SwiftUI — 11 141 rader i 53 filer — ovanpå C‑gränssnittet till en Rust‑kärna som länkas som ett statiskt arkiv från en låst revision.", "summary": "En nativ klient som startar som en Mac-app, använder plattformens egna material och kontroller och inte bär på någon inbäddad webbläsare.", "content_html": "<p><strong>Situation.</strong> Meddelandeprotokollet hade en vältestad kärna skriven i Rust och klienter på flera plattformar, men skrivbordsupplevelsen på macOS var ett plattformsoberoende skal — den såg inte ut som en Mac‑app, betedde sig inte som en och bar på en körmiljö som en nativ app inte behöver. Kärnan exponerar sin funktionalitet via ett C‑gränssnitt, vilket innebär att varje språk som kan anropa C kan bygga på den, och det kan Swift.</p>\n<p><strong>Uppgift.</strong> En nativ klient måste byggas direkt på det C‑gränssnittet, i plattformens nuvarande språk och nuvarande gränssnittsramverk, med kärnan inlänkad i stället för levererad vid sidan av.</p>\n<p><strong>Handling.</strong> Appen består av 11 141 rader Swift i femtiotre filer i appens target, byggd på SwiftUI med kärnan länkad som ett statiskt arkiv kompilerat från en fastlåst uppströmsrevision. Att låsa revisionen i stället för att följa en gren är det beslut som gör bygget reproducerbart: C‑gränssnittet är kontraktet, och en kärna i rörelse ändrar kontraktet utan att en enda rad Swift ändras. Skiktningen håller den osäkra ytan liten — en tunn wrapper äger varje pekare och varje sträng som korsar gränsen, modellerna ovanför är vanliga Swift‑värden och gränssnittsskiktet ser aldrig en rå pekare. Allt som C‑gränssnittet returnerar har en ägarskapsregel, och ett misstag i någon av dem ger antingen en läcka eller en krasch utan en enda kompilatorvarning på vägen, så det är i wrappern som hela projektets minnesdisciplin finns. Bygget drivs av en task runner med femtiosex mål som täcker kompileringen av kärnan, Swift‑bygget, testsviterna, lintverktygen och paketeringen.</p>\n<p><strong>Resultat.</strong> En nativ klient som startar som en Mac‑app, använder plattformens egna material och kontroller och inte bär på någon inbäddad webbläsare. Det som inte är gjort bör sägas rakt ut: detta är ingen levererad release. Det finns ingen notariserad distribution, ingen uppdateringskanal och bara ett språk i gränssnittet, så det är en fungerande app snarare än en produkt som någon annan kör.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Designsystem och UI", "Frontendutveckling", "Fullstackutveckling", "Lösningsarkitektur", "UX/UI‑design", "Fullstackutveckling av produkter", "Plattforms- och lösningsarkitektur", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/", "url": "https://engineer.company/sv/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/", "title": "Stoppade en applikation som fyllde minnet med 41 MB i sekunden — uppmätt 111 GB komprimerade sidor på en maskin med 36 GB — genom att begränsa varje händelseström, prenumerera per händelsetyp och sätta en hastighetsbudget på loggningen, vilket tog ned 610 996 loggrader till 1 411.", "summary": "Minnesanvändningen ligger platt under ihållande last, och samma session som gav 610 996 loggrader ger nu 1 411.", "content_html": "<p><strong>Situation.</strong> Appen fyllde minnet tills operativsystemet dödade den. Den registrerade incidenten nådde 111 GB komprimerade minnessidor på en maskin med 36 GB, med en ökning på ungefär 41 MB i sekunden, och loggfilen för en enda kort session innehöll 610 996 rader. En maskin i det tillståndet är inte långsam, den är oanvändbar — processen dödas först när swappningen redan har fått allt annat på skrivbordet att sluta svara.</p>\n<p><strong>Uppgift.</strong> Tillväxten måste hittas i stället för att gissas, och varje obegränsad väg måste få en gräns, eftersom en begränsad kö bredvid tre obegränsade inte är någon lösning.</p>\n<p><strong>Handling.</strong> Det fanns tre orsaker som multiplicerade varandra, och multiplikationen är förklaringen till att det gick så fort. Händelseströmmen från kärnan konsumerades utan någon gräns, så händelserna kom in snabbare än gränssnittet hann tillämpa dem, och eftersläpningen behölls i stället för att kastas. Varje prenumerant tog emot varje händelse och filtrerade i efterhand, så kostnaden för en händelse multiplicerades med antalet lyssnare, och varje lyssnares filtrering allokerade minne. Och loggningen saknade budget, så varje händelse gav upphov till loggrader — vilket är den term som förstärker allt, eftersom loggvolymen var proportionell mot omfattningen av det som gick fel. Lösningen tog itu med alla tre: begränsade buffertar med en uttrycklig policy för vad som händer när de blir fulla, prenumeration per händelsetyp så att en lyssnare bara väcks för de händelser den vill ha, och en taktbudget för loggningen som slår ihop upprepningar i stället för att skriva var och en. Ett regressionstest driver en hög händelsetakt och verifierar att minnestaket håller, så gränsen är en egenskap hos bygget snarare än en kommentar.</p>\n<p><strong>Resultat.</strong> Minnesanvändningen ligger platt under ihållande last, och samma session som gav 610 996 loggrader ger nu 1 411. Det ärliga förbehållet är att taktbudgeten för loggningen kastar information: när något går fel snabbt är spåren av det numera medvetet ofullständiga, och det är en avvägning som gjorts med öppna ögon mot alternativet, en maskin som stannar.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Frontendutveckling", "Lösningsarkitektur", "Prestandaoptimering", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Plattforms- och lösningsarkitektur", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/", "url": "https://engineer.company/sv/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/", "title": "Införde fullständig strikt samtidighet i Swift 6 utan actors och överbryggade en blockerande händelseloop i C till main actor genom en producent, en konsument och en ordning — efter att ha konstaterat att en task per händelse förlorar den ordning som gränssnittet är beroende av.", "summary": "Appen kompilerar med fullständig strikt samtidighetskontroll utan några undertryckta varningar, och händelseordningen är en strukturell egenskap snarare än en…", "content_html": "<p><strong>Situation.</strong> Den fullständiga strikta samtidighetskontrollen i Swift 6 gör datakapplöpningar till kompileringsfel i stället för sporadiska krascher. Det är när den införs mot ett C‑bibliotek som det blir svårt: kärnans händelseloop är ett blockerande anrop som måste köras utanför huvudtråden för all framtid, och värdena den lämnar tillbaka är pekare utan några som helst samtidighetsgarantier. Kompilatorn kan inte resonera om något av det och vägrar allt tills gränsen är uttryckligen beskriven.</p>\n<p><strong>Uppgift.</strong> Fullständig kontroll måste slås på utan nödutgångar, vilket innebar att utforma övergången från en blockerande C‑loop till main actor i stället för att annotera sig runt den.</p>\n<p><strong>Handling.</strong> Det självklara angreppssättet är en actor per delsystem, och det förkastades efter mätning snarare än av smakskäl. Att starta en task per inkommande händelse låter körmiljön schemalägga dem i godtycklig ordning, och kärnans händelseström är ordnad — en händelse om att ett meddelande har ändrats som går om händelsen om att samma meddelande har skapats ger ett gränssnitt som visar en redigering av något som ännu inte finns. Att actors är återinträdesbara gör saken värre, inte bättre, eftersom en actor kan pausa mitt i en metod och behandla ett annat anrop. Det som ersatte den är medvetet enkelt: en producenttråd som äger den blockerande loopen, en konsument, en kö mellan dem och ett enda hopp över till main actor på slutet. Ordningen bevaras eftersom det finns exakt en väg och ingenting går om något annat. De osäkra typer som korsar den gränsen kapslas in i typer vars trådsäkerhet intygas i wrappern i stället för att antas, och intygandet är dokumenterat tillsammans med skälet till att det håller — pekaren ägs av en enda tråd och kopieras innan den lämnas över.</p>\n<p><strong>Resultat.</strong> Appen kompilerar med fullständig strikt samtidighetskontroll utan några undertryckta varningar, och händelseordningen är en strukturell egenskap snarare än en förhoppning. Priset är att designen är mindre parallell än den skulle kunna vara: allt kanaliseras genom en enda konsument, och om den konsumenten någon gång blir en flaskhals kräver lösningen att man på nytt härleder vilka händelser som säkert kan byta ordning, vilket är precis den analys som det här undvek.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Backendutveckling", "Frontendutveckling", "Lösningsarkitektur", "Prestandaoptimering", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "Plattforms- och lösningsarkitektur", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/", "url": "https://engineer.company/sv/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/", "title": "Skrev en parser som läser den verkliga C‑headern på 7 308 rader och verifierar varje anropsställe, varje enum‑konstant och att varje klass som äger en pekare är final, efter att en handskriven platshållarheader hade låtit anrop till tre borttagna funktioner kompilera, länka och krascha.", "summary": "Den feltyp som orsakade den ursprungliga kraschen kan inte återkomma, eftersom en inaktuell referens nu är ett byggfel i stället för ett körtidsfel.", "content_html": "<p><strong>Situation.</strong> Tidigt i projektet representerades C‑gränssnittet av en handskriven headerfil som beskrev de funktioner som applikationen förväntade sig. Den headerfilen kompilerade, applikationen länkades, och anrop till tre funktioner som inte längre fanns i kärnan kom ända fram till att anropas och kraschade. Både kompilatorn och länkaren hade nöjt sig med en beskrivning av biblioteket i stället för biblioteket självt, och glappet visade sig först vid körning.</p>\n<p><strong>Uppgift.</strong> Den riktiga headerfilen — 7 308 rader och 268 deklarationer — måste bli auktoriteten, och varje användning av den i Swift‑koden måste verifieras mot den automatiskt i stället för genom granskning.</p>\n<p><strong>Handling.</strong> Kontrollen är en parser som läser den faktiska headerfilen från uppströms och bygger upp mängden av de funktioner, enum‑konstanter och typer den deklarerar, och sedan läser Swift‑källkoden och slår upp varje anropsställe och varje konstantreferens mot den mängden. Ett anrop till en funktion som headerfilen inte deklarerar får bygget att fallera. En referens till en enum‑konstant som har bytt namn får bygget att fallera. Det aktuella antalet är 132 av de 268 deklarationerna som refereras, och att veta vilka 136 som inte används är i sig användbart, eftersom det säger exakt hur stor del av kärnan som klienten inte har nått. Parsern upprätthåller också en regel som kompilatorn inte kan: varje Swift‑klass som äger en pekare in i kärnan måste vara final. En klass som äger en pekare och inte är final kan ärvas, och en subklass som åsidosätter deinitieringen eller lägger till en egen livslängd ändrar när pekaren frigörs — en use‑after‑free utan ett enda unsafe‑nyckelord i närheten. Regeln kontrolleras med namn över hela trädet.</p>\n<p><strong>Resultat.</strong> Den feltyp som orsakade den ursprungliga kraschen kan inte återkomma, eftersom en inaktuell referens nu är ett byggfel i stället för ett körtidsfel. Begränsningen är att parsern förstår headerfilens deklarationer men inte dess semantik: den bevisar att en funktion med ett matchande namn finns, och en funktion vars betydelse eller ägarskapsregel har ändrats uppströms medan signaturen behållits passerar utan anmärkning.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Backendutveckling", "Dokumentation", "Säkerhet", "Test och kvalitetssäkring", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/sv/portfolio/took-the-test-suite-under-nine-seconds-158/", "url": "https://engineer.company/sv/portfolio/took-the-test-suite-under-nine-seconds-158/", "title": "Tog testsviten från sex tester över en gräns på sextio sekunder till 135 godkända på 8,9 sekunder genom att profilera huvudtråden och ta bort de två anrop som den befann sig i vid 3 989 av 4 017 stickprov.", "summary": "Sviten gick från sex tester över sextio sekunder — 74 sekunders väggklockstid — till 135 tester som går igenom på 8,9, vilket placerar den inom det fönster där…", "content_html": "<p><strong>Situation.</strong> Testsviten hade en tidsgräns på sextio sekunder och sex tester låg över den. En testsvit som tar över en minut slutar köras före varje ändring, och en svit som inte körs före varje ändring är en rapport om det förflutna. Instinkten i det läget är att höja gränsen, och det är genom att höja gränsen som en svit hamnar på tio minuter.</p>\n<p><strong>Uppgift.</strong> Tiden måste hittas i stället för att budgeteras, vilket innebar att profilera sviten i stället för att resonera om vilka tester som såg dyra ut.</p>\n<p><strong>Handling.</strong> Profilen togs på huvudtråden medan sviten kördes, och resultatet motsade gissningen. Huvudtråden befann sig inne i två anrop i 3 989 stickprov av 4 017 — så de långsamma testerna var inte de som gjorde mest arbete, och nästan varje test passerade genom samma två ställen. Det första var en fast väntan som skulle låta asynkront arbete hinna bli klart före kontrollerna — i praktiken en sleep, som betalades av varje test som rörde händelsevägen oavsett om arbetet redan var klart eller inte. Den ersattes med väntan på det faktiska villkoret med en tidsgräns, så att ett test som är klart på fem millisekunder tar fem millisekunder och bara ett test som verkligen har fastnat betalar hela väntan. Det andra var ett förberedelsesteg per test som byggde om en dyr fixtur varje gång, trots att fixturen bara lästes och kunde byggas en gång för hela sviten. Inget av dem låg i ett test som någon skulle ha pekat ut som långsamt; båda låg i den gemensamma vägen, vilket är skälet till att hela sviten var jämnt långsam i stället för att ett fåtal tester stack ut.</p>\n<p><strong>Resultat.</strong> Sviten gick från sex tester över sextio sekunder — 74 sekunders väggklockstid — till 135 tester som går igenom på 8,9, vilket placerar den inom det fönster där den körs vid varje sparning. Förbehållet är att den gemensamma, skrivskyddade fixturen nu är en kopplingspunkt — ett test som ändrar den ger ett fel i ett annat test, och svitens hastighet beror på en disciplin som kompilatorn inte upprätthåller.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "DevOps", "Prestandaoptimering", "Test och kvalitetssäkring", "DevOps och CI/CD‑automatisering", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/named-all-seventeen-interface-icons-for-screen-readers-159/", "url": "https://engineer.company/sv/portfolio/named-all-seventeen-interface-icons-for-screen-readers-159/", "title": "Gav varje kontroll i gränssnittet som bara består av en ikon ett namn för skärmläsare efter att ha hittat skicka‑knappen uppläst som ”arrow up circle, button”, och skrev lintern som kräver etiketten inom åtta rader från ikonen.", "summary": "Alla sjutton kontroller presenterar sin funktion, och en artonde kan inte läggas till utan en. Regelns oprecision är verklig och angiven där regeln definieras:…", "content_html": "<p><strong>Situation.</strong> Gränssnittet använde systemsymboler för sina kontroller, och en symbol utan tillgänglighetsetikett läses upp av skärmläsaren med sitt eget interna namn. Skicka‑knappen presenterades som ”arrow up circle, button”. Detsamma gällde varje annan kontroll med enbart ikon i applikationen, var och en på sitt sätt — sjutton stycken som beskrev sin egen grafik i stället för sin funktion.</p>\n<p><strong>Uppgift.</strong> Varje kontroll med enbart ikon behövde en etikett som beskriver vad den gör, och åtgärden måste åtföljas av en automatisk kontroll, eftersom nästa ikon som lades till annars omedelbart skulle föra tillbaka felet.</p>\n<p><strong>Handling.</strong> Var och en av de sjutton fick en etikett som namnger handlingen snarare än formen, och där kontrollens betydelse beror på tillstånd följer etiketten tillståndet i stället för att vara fast. Den automatiska kontrollen är den del som är värd att beskriva, eftersom det inte finns någon allmän regel av typen ”är det här tillgängligt” för det här. Lintverktyget letar efter den konstruktion som skapar en kontroll med enbart ikon och kräver sedan en tillgänglighetsetikett inom åtta rader från den. Åtta rader är en medvetet grov heuristik, och den valdes genom mätning: den är lång nog att täcka varje legitimt sätt som kodbasen skriver en sådan kontroll med sina modifierare på, och kort nog att en etikett som hör till en annan vy längre ned inte råkar uppfylla den. En exakt regel här skulle behöva förstå SwiftUI:s kedjor av modifierare som ett träd, och den billiga närhetsregeln fångar det faktiska misstaget — som inte är en felmärkt kontroll, utan en omärkt.</p>\n<p><strong>Resultat.</strong> Alla sjutton kontroller presenterar sin funktion, och en artonde kan inte läggas till utan en. Regelns oprecision är verklig och angiven där regeln definieras: den kan uppfyllas av en etikett på en närliggande vy, så den bevisar att det finns en etikett i närheten snarare än att etiketten är korrekt, och korrektheten kräver fortfarande att någon lyssnar på den.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Dokumentation", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/built-a-48-token-design-system-160/", "url": "https://engineer.company/sv/portfolio/built-a-48-token-design-system-160/", "title": "Byggde ett designsystem med 48 tokens på plattformens eget glasmaterial och skrev lintern som avvisar ett magiskt tal, en hårdkodad teckenstorlek eller en animering som ignorerar inställningen för reducerad rörelse.", "summary": "Fyrtioåtta tokens beskriver hela gränssnittet, utseendet följer systemet och ingen av de tre erosionerna kan committas.", "content_html": "<p><strong>Situation.</strong> Ett gränssnitt som byggs genom att vyer läggs till samlar på sig värden: en hörnradie här, ett typsnitt på fjorton punkter där, en animation på två tiondels sekund någon annanstans. Vart och ett är rimligt för sig och tillsammans utgör de en design som inte går att ändra, eftersom det inte finns något som heter ”hörnradien” att ändra — det finns fyrtio av dem, lite olika, utspridda över femtio filer.</p>\n<p><strong>Uppgift.</strong> Det visuella ordförrådet måste reduceras till en namngiven uppsättning, uttryckt med plattformens eget material i stället för att uppfinnas på nytt, och försvaras av en kontroll så att det förblir reducerat.</p>\n<p><strong>Handling.</strong> Systemet består av fyrtioåtta tokens i sex grupper: avstånd, typografi, färg, radie, höjdnivå och rörelse. Färg och material bygger på plattformens semantiska färger och dess glasmaterial i stället för fasta värden, vilket är det som får gränssnittet att följa systemets utseende, accentfärgen och kontrastinställningarna utan någon kod som bevakar dem. Typografin mappas till plattformens textstilar så att den skalas med användarens storleksinställning i stället för att låsa ett punktvärde. Lintverktyget avvisar tre saker: en numerisk literal där en token för avstånd eller radie hör hemma, en hårdkodad teckenstorlek var som helst, och en animation som deklareras utan att respektera inställningen för minskad rörelse. Den tredje är den som annars skulle eroderas snabbast, eftersom en animation läggs till i ett ögonblick av finputsning och kontrollen av inställningen är den del som hoppas över — så regeln gör det omöjligt att skriva själva animationen utan den, i stället för att be någon komma ihåg den.</p>\n<p><strong>Resultat.</strong> Fyrtioåtta tokens beskriver hela gränssnittet, utseendet följer systemet och ingen av de tre erosionerna kan committas. Begränsningen är att ett tokensystem styr konsekvens och inte kvalitet — allt matchar nu, och att matcha är inte detsamma som att vara väldesignat, vilket är en bedömning som inget lintverktyg i det här projektet gör.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Designsystem och UI", "Frontendutveckling", "Test och kvalitetssäkring", "UX/UI‑design", "UI/UX‑design och designsystem", "Varumärke, marknadsföring och SEO" ] }, { "id": "https://engineer.company/sv/portfolio/reached-the-unused-half-of-the-messaging-core-161/", "url": "https://engineer.company/sv/portfolio/reached-the-unused-half-of-the-messaging-core-161/", "title": "Tog i bruk den hälft av meddelandekärnan som applikationen aldrig hade använt — överföring av säkerhetskopior, försvinnande meddelanden, redigering och omsändning av meddelanden, verifierade inbjudningar, proxyer och krypteringspolicy — och körde varje test mot det riktiga biblioteket utan mockar.", "summary": "Den tidigare onådda hälften av kärnan körs av tester mot det riktiga biblioteket, och wrappern har det högsta golvet i projektet.", "content_html": "<p><strong>Situation.</strong> Klienten använde ungefär hälften av det som meddelandekärnan erbjuder. Den oanvända hälften var inte obskyr — överföring av säkerhetskopior mellan enheter, försvinnande meddelanden, redigering och omsändning av meddelanden, verifierade inbjudningslänkar, proxykonfiguration och krypteringspolicyn för en chatt. Var och en är en funktion som en användare skulle förvänta sig och var och en var ett otestat område av C‑gränssnittet, vilket är det farligare faktumet, eftersom ett otestat område av ett C‑gränssnitt är där ägarskapsmisstagen finns.</p>\n<p><strong>Uppgift.</strong> Den onådda kapaciteten måste köras och täckas, och testerna måste köras mot det riktiga biblioteket i stället för mot en ersättare.</p>\n<p><strong>Handling.</strong> Regeln som infördes var inga mockar för kärnan. En mock av ett C‑gränssnitt kodar in utvecklarens uppfattning om vad biblioteket gör, och varje fel som är värt att hitta här är ett ställe där den uppfattningen är fel — så ett godkänt mockbaserat test säger något om mocken. I stället skapar testerna riktiga konton i tillfälliga kataloger, kör det riktiga biblioteket och kontrollerar vad det faktiskt returnerar, där varje svit städar upp sitt eget tillstånd. Det var det som gjorde testtäckningen meningsfull: att köra överföringen av säkerhetskopior innebar att hantera en riktig överförings tillståndsmaskin och dess felvägar, och att köra verifierade inbjudningar innebar att konstruera det riktiga länkformatet och låta biblioteket tolka det. Golv för testtäckningen sattes per skikt i stället för som ett enda tal, till åttiofyra procent för kärnans wrapper, sjuttiofem för verktygsfunktioner och sextiofem för modeller, med resonemanget att skiktet som rör råa pekare ska hållas högst och att en modell som mest består av lagrade egenskaper inte ska fyllas ut med tester för att nå ett genomsnitt.</p>\n<p><strong>Resultat.</strong> Den tidigare onådda hälften av kärnan körs av tester mot det riktiga biblioteket, och wrappern har det högsta golvet i projektet. Kostnaden är hastighet och determinism: tester mot det riktiga biblioteket är långsammare än mockar och kan fallera av miljöskäl, vilket är priset för att de kan fallera av verkliga skäl.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "API:er och integration", "Automatisering och CI/CD", "Backendutveckling", "Säkerhet", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Backend- och API‑utveckling", "DevOps och CI/CD‑automatisering", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/", "url": "https://engineer.company/sv/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/", "title": "Skrev om applikationens felmeddelanden enligt en skriftlig tonstandard efter att en nekad inloggning hade skyllt på användaren för felskrivning när leverantören i själva verket krävde ett appspecifikt lösenord, och täckte det med ett test som namnger leverantören.", "summary": "Felmeddelandena följer en uttalad standard och fallet som gav upphov till den täcks av ett test som fallerar på den gamla texten.", "content_html": "<p><strong>Situation.</strong> En inloggning mot en stor e‑postleverantör misslyckades, och applikationen sa till användaren att lösenordet var fel. Det var inte fel. Den leverantören kräver ett applikationsspecifikt lösenord för klienter från tredje part och avvisar kontots lösenord hur noggrant det än skrivs. Meddelandet skickade användaren till att skriva om något som aldrig kunde fungera, och den faktiska instruktionen — gå och skapa en annan sorts lösenord — fanns ingenstans.</p>\n<p><strong>Uppgift.</strong> Felmeddelandena måste skrivas om mot en nedskriven standard i stället för att lappas ett i taget, eftersom det här var det synliga exemplet på en vana som löpte genom dem alla.</p>\n<p><strong>Handling.</strong> Standarden har tre krav: säg vad som hände, antyd aldrig att användaren gjorde något fel när orsaken ligger någon annanstans, och ange nästa steg när det finns ett. Tillämpad på alla felmeddelanden bröt de flesta mot minst ett krav — flera var det underliggande bibliotekets felsträng som skickades vidare, vilket beskriver ett tillstånd för en programmerare snarare än en situation för en människa. Leverantörsfallet skrevs om så att det namnger leverantören, anger att den kräver ett appspecifikt lösenord för andra klienter och säger var man skapar ett. Testet är det som hindrar regression, och det är medvetet specifikt: det framkallar ett inloggningsfel mot den leverantören och kontrollerar att meddelandet innehåller leverantörens namn och den fras som beskriver vilken typ av inloggningsuppgift som krävs. Ett test som bara kontrollerade att något fel visades skulle godkänna det ursprungliga felaktiga meddelandet, så kontrollen gäller innehållet, som är den enda del som någonsin var trasig.</p>\n<p><strong>Resultat.</strong> Felmeddelandena följer en uttalad standard och fallet som gav upphov till den täcks av ett test som fallerar på den gamla texten. Det som återstår olöst är skalan: standarden upprätthålls genom granskning och genom ett test på ett meddelande, och de andra leverantörerna med sina egna särskilda krav har inget motsvarande test, så felklassen är dokumenterad snarare än stängd.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Dokumentation", "Frontendutveckling", "Produkt och krav", "Säkerhet", "Test och kvalitetssäkring", "UX/UI‑design", "Produktstrategi och krav", "Teknisk dokumentation", "UI/UX‑design och designsystem" ] }, { "id": "https://engineer.company/sv/portfolio/audited-644-rust-crates-for-licence-compatibility-163/", "url": "https://engineer.company/sv/portfolio/audited-644-rust-crates-for-licence-compatibility-163/", "title": "Granskade licenskompatibiliteten hos 644 Rust‑crates vid varje bygge och bevisade att kontrollen slår till genom att skriva om licensen för en crate och genom att flytta den låsta kärnrevisionen utan att generera om.", "summary": "Distributionsläget kontrolleras vid varje bygge, och det är känt att kontrollen fungerar eftersom den fick fallera, inte för att den aldrig har sagt något.", "content_html": "<p><strong>Situation.</strong> Att länka in en Rust‑kärna i en levererad applikation innebär att leverera allt som kärnan är beroende av. Beroendegrafen omfattar 644 crates. Var och en har en licens, vissa har mer än en, och en enda copyleft‑crate som dyker upp tre nivåer ned genom en rutinmässig versionshöjning ändrar under vilka villkor applikationen som helhet får distribueras — i tysthet, i en låsfil som ingen läser rad för rad.</p>\n<p><strong>Uppgift.</strong> Licensläget måste verifieras vid varje bygge i stället för att granskas en gång, med en uttrycklig tillåtelselista, så att en ändring i grafen blir ett byggfel och inte en upptäckt som någon annan gör senare.</p>\n<p><strong>Handling.</strong> Licensgranskningen löser upp hela den transitiva grafen och kontrollerar varje crates licensuttryck mot en lista över villkor som projektet godtar, och utvärderar de booleska uttrycken korrekt — en crate som erbjuder ett val mellan två licenser godtas om någon av dem finns på listan, och en crate som kräver båda godtas bara om båda gör det. Allt som inte matchar får bygget att fallera i stället för att ge en varning, och att lägga till ett villkor i tillåtelselistan är en avsiktlig ändring med ett skäl. En kontroll som aldrig har fallerat går inte att skilja från en som inte kan fallera, så den fick fallera med flit, två gånger. En gång genom att en crates licensuttryck skrevs om till något som listan inte godtar, vilket är formen för en crate som ändrar sina villkor uppströms mellan versioner — det fall som ingen mänsklig granskning fångar, eftersom craten i sig inte är ny. En gång genom att den fastlåsta kärnrevisionen flyttades utan att licensgranskningen genererades om, vilket är formen för en höjning av kärnan som i tysthet för med sig ett nytt beroende. Båda provokationerna fick bygget att fallera som avsett, och kontrollen lämnades kvar i stället för att vidgas.</p>\n<p><strong>Resultat.</strong> Distributionsläget kontrolleras vid varje bygge, och det är känt att kontrollen fungerar eftersom den fick fallera, inte för att den aldrig har sagt något. Innan den fanns gjorde paketet, licensfilen och projektets egen README alla ett påstående om 644 crates som ingenting hade verifierat. Begränsningen är precis och bör inte överdrivas: kontrollen läser de deklarerade licensmetadata, och metadata kan vara felaktiga eller ofullständiga. Den bevisar ingenting om en crate som deklarerar sig felaktigt, och den är ingen juridisk rådgivning.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Datastyrning", "DevOps", "Dokumentation", "Säkerhet", "Datastyrning och datakvalitet", "DevOps och CI/CD‑automatisering", "Säkerhet och behörighetshantering" ] }, { "id": "https://engineer.company/sv/portfolio/held-five-repositories-to-one-history-standard-165/", "url": "https://engineer.company/sv/portfolio/held-five-repositories-to-one-history-standard-165/", "title": "Lät fyra repon följa en och samma historikstandard — konventionella commits, fria från emojier, utan attributionsrader, upprätthållen av en hook för commit‑meddelanden — vid sidan av 48 instruktionsdokument som styr hur arbetet utförs.", "summary": "Fyra repon delar ett historikformat och en instruktionsstruktur, båda upprätthållna av hookar snarare än av disciplin.", "content_html": "<p><strong>Situation.</strong> Fyra repon byggda under arton månader av en enda person är det läge där process är lättast att hoppa över, eftersom det inte finns någon att samordna med och kostnaden för en oläslig historik betalas helt av ett framtida jag som ännu inte har klagat. Det är också det läge där en inkonsekvent historik är mest sannolik, eftersom varje repo kan glida in i sina egna vanor utan att något drar dem samman.</p>\n<p><strong>Uppgift.</strong> En standard för historik och en standard för instruktioner måste gälla i varje repo som arbetet skrivs i, och den måste upprätthållas maskinellt i stället för att man ska komma ihåg den.</p>\n<p><strong>Handling.</strong> Commitstandarden är ett konventionellt prefix som anger typen av ändring och ett scope, en ämnesrad under en fast längd, inga emojier och inga tillskrivningsrader av något slag — det sista eftersom en rad som krediterar ett verktyg inte är ett faktum om ändringen, och historiken är till för fakta om ändringar. En hook för commitmeddelanden avvisar allt som inte följer standarden, i varje repo där arbete sker, så standarden är en egenskap hos repot snarare än hos den som committar. Fördelningen i det största repot visar vad arbetet faktiskt var: 156 commits för funktioner, 133 rättningar, 128 för dokumentation, 81 för underhållssysslor, 26 refaktoreringar, 20 för stil, 5 för prestanda och 3 för test. Dokumentationen ligger så nära rättningarna att det är värt att notera, och det är en följd av den andra halvan av det här — 48 instruktionsdokument i de fyra repona, vart och ett täcker en domän, vart och ett är skrivet som regler snarare än beskrivning, och alla nås från ett enda ingångsdokument per repo så att det finns ett ställe att börja på. Ett lintverktyg för dokumentation upprätthåller storleksbudgetar per fil och indexmedlemskap, så att samlingen förblir navigerbar i stället för att växa till ett arkiv.</p>\n<p><strong>Resultat.</strong> Fyra repon delar ett historikformat och en instruktionsstruktur, båda upprätthållna av hookar snarare än av disciplin. Det som detta inte gör är att göra historiken bra: formatet kontrolleras men innehållet gör det inte, så en ämnesrad som följer standarden men inte beskriver någonting passerar precis lika väl som en som förklarar ändringen.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Agilt arbete och Scrum", "Automatisering och CI/CD", "DevOps", "Dokumentation", "Projektledning", "Tekniskt ledarskap", "DevOps och CI/CD‑automatisering", "Projektledning (agil)", "Teknisk dokumentation", "Tekniskt ledarskap och rådgivning" ] }, { "id": "https://engineer.company/sv/portfolio/measured-thirteen-addresses-from-the-access-log-alone-166/", "url": "https://engineer.company/sv/portfolio/measured-thirteen-addresses-from-the-access-log-alone-166/", "title": "Mätte tretton adresser enbart utifrån webbserverns egen åtkomstlogg — ingen cookie, ingen spårare och ingen tjänst från tredje part — maskerade klientens nätverk när varje rad skrivs och förde in resultatet i ett permanent arkiv med 21 dagliga räkningar och 20 månatliga fasetter.", "summary": "Tretton adresser mäts på det här sättet, och webbplatsen levererar ingen kaka, ingen spårare och ingen förfrågan till tredje part, så det finns ingenting att…", "content_html": "<p><strong>Situation.</strong> Webbplatsen hade inget sätt att veta om någon läste den. Varje vanligt svar på det kostar läsaren något — en kaka, ett skript, en pixel, en hostad tjänst som får veta något om en besökare i förbifarten — och vart och ett av dem avvisades innan den första raden av det här skrevs. Det som återstod var webbserverns egen åtkomstlogg, den enda registrering som finns ändå, eftersom en förfrågan måste besvaras.</p>\n<p><strong>Uppgift.</strong> Frågan var om en användbar bild av läsekretsen går att bygga av en loggrad som redan har fått de identifierande delarna borttagna, och om borttagningen kan bevisas i stället för att utlovas.</p>\n<p><strong>Handling.</strong> Maskeringen sker när raden skrivs, inte i en städkörning efteråt, eftersom ett löfte om att inte lagra något bryts av att det lagras och städas bort senare. Servern härleder klientadressen under två namn, och båda maskeras — att maskera det ena ser korrekt ut i konfigurationen medan den fullständiga adressen ligger på disk under det andra, vilket bara en sondering mot den körande servern hittade. Sju headers för vidarebefordrade adresser kastas helt, eftersom kodaren skriver headertabellen exakt som klienten skickade den, och en besökare bakom en företagsproxy annars skulle föra in sin egen fullständiga adress i filen under ett namn som ingen mask någonsin tittade på. Cookie‑headern, authorization‑headern och den tillfälliga källporten går samma väg, och frågesträngar skärs bort innan något läser sökvägen, så varken de ord någon skrev i en sökmotor eller de ord någon skrev i webbplatsens egen sökruta kan nå någon fil. Insamlingen sker genom hämtning: ett skript som bara använder standardbiblioteket strömmar den aktiva loggen och dess roterade föregångare på värden och skriver ut räknevärden i stället för rader, begränsat av antalet distinkta sökvägar i stället för av loggens storlek, eftersom maskinen har 464 MB. Lagringstiden krävde två mekanismer i stället för en, sedan ett storlekstak och ett tidstak på samma hylla visat sig innebära att det mindre vinner i tysthet — en välbesökt webbplats behöll tio dagar i stället för trettio, och en lugn raderade ingenting alls. Det som överlever loggen är ett arkiv med 21 räknevärden per dag och 20 facetter per månad, som slås samman i stället för att läggas till enligt regeln att ett fönster bara kan se för lite, med totalen lagrad bredvid varje rangordning så att en avkortad tabell redovisar hur mycket den utelämnade.</p>\n<p><strong>Resultat.</strong> Tretton adresser mäts på det här sättet, och webbplatsen levererar ingen kaka, ingen spårare och ingen förfrågan till tredje part, så det finns ingenting att samtycka till och ingenting för en tagg att köras i. Designen vägrar mer än den besvarar — unika besökare, söktermer, en besökares väg genom webbplatsen, tid på sidan, klick, geografi och enhet är samtliga obesvarbara här och redovisas som sådana — och begränsningarna skrivs ut bredvid siffrorna i stället för att hållas som finstilt, eftersom en grov siffra som läses som en exakt är värre än ingen siffra.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Datastyrning", "Infrastruktur", "Python", "Säkerhet", "Övervakning och observerbarhet", "Datastyrning och datakvalitet", "Säkerhet och behörighetshantering", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/made-six-collectors-state-what-they-never-read-167/", "url": "https://engineer.company/sv/portfolio/made-six-collectors-state-what-they-never-read-167/", "title": "Fick sex rapporterande insamlare att ange den del av sin indata som de aldrig läste — filer som förblev stängda, sonder som inte installerats, journalrader som inte kändes igen — och anpassade tröskelvärden till 116 mätningar, efter att ha konstaterat att en blind insamling och en värd i gott skick ger samma sida.", "summary": "En sida med den här plattformens siffror bär nu sina egna blinda fläckar bredvid sina fynd. Ingen del av självmätningen är en grind, och det sägs uttryckligen…", "content_html": "<p><strong>Situation.</strong> Sex insamlare som bara läser renderade en värd i drift till Markdown, och inte en enda av dem registrerade sin egen körtid, de filer den inte kunde öppna eller de indata den hoppade över. Flera hade felvägar som svalde felet helt, vilket ger en specifik och farlig sorts rapport: en sida med siffror som ser fullständig och frisk ut oavsett om något faktiskt lästes eller inte. En roterad logg som kommer fram avkortad tar bort hela sitt tidsspann ur varje siffra på sidan, och en vecka med saknad trafik ser exakt ut som en lugn vecka. En spegel vars daemon ändrade sitt loggformat ger samma nolla som en spegel som ingen besökte. Varje sondering i säkerhetsrapporten ligger bakom en kontroll av att verktyget finns, så en värd som saknade brandväggs-, socket- och spärrverktygen renderade inga regler, inga sockets och inga jails — den visuella signaturen för en nedlåst värd, och i själva verket en blind.</p>\n<p><strong>Uppgift.</strong> Två saker måste bli omöjliga att förväxla med god hälsa. En rapport måste ange vad som räknas som dåligt i stället för att lämna åt en människa att avgöra det, och varje insamlare måste ange vad dess egen insamling kostade och hur mycket av dess indata den inte kunde använda.</p>\n<p><strong>Handling.</strong> Tröskelvärdena anpassades efter de 116 stickprov som metrikrapporten redan hade mätt, aldrig efter en gissning, med de observerade intervallen registrerade bredvid gränserna. Bedömningen hittar varje kolumn med namn i stället för med position och vägrar bedöma en rad vars antal värden inte stämmer med dess antal etiketter, vilket är det som skyddar den mot en ändring av klockformatet som förskjuter varje etikett ett steg. Rapporten skiljer sedan på tre utfall i stället för två: en överträdelse, ingen överträdelse, och ej bedömd eftersom ingenting mätte den. På insamlingssidan rapporterar var och en nu sin körtid och sin blindhet — lästa filer mot oläsbara filer, lästa journalrader mot igenkända journalrader, repon som git vägrade läsa räknade i stället för återgivna som en nolla, tillfrågade källor mot källor som svarade, efterfrågade sonderingar mot sonderingar som inte är installerade — och var och en av de raderna skrivs ut även när svaret är noll, eftersom en rad som försvinner när den är tom är samma tystnad i en annan form. Varje mall fick en uttrycklig gren för utdata som skrivits av en insamlare som är äldre än mallen själv.</p>\n<p><strong>Resultat.</strong> En sida med den här plattformens siffror bär nu sina egna blinda fläckar bredvid sina fynd. Ingen del av självmätningen är en grind, och det sägs uttryckligen i stället för att antydas: ingen gräns är anpassad efter en insamlares körtid, så en insamling som tar dubbelt så lång tid syns men stoppar ingen, och att anpassa en sådan gräns skulle kräva en baslinje från den riktiga värden som det här arbetet medvetet inte hittade på. De gränser som finns är dagsgenomsnitt, så en värd som når ett tak under en minut passerar ingenting här — dödmansgreppet på femton minuter fångar det, inget av dem ersätter det andra, och båda rapporterna säger det på sidan.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Automatisering och CI/CD", "Dokumentation", "Python", "Test och kvalitetssäkring", "Tillförlitlighet och säkerhetskopiering", "Övervakning och observerbarhet", "Systemadministration", "Teknisk dokumentation", "Tillförlitlig drift och övervakning" ] }, { "id": "https://engineer.company/sv/portfolio/published-the-site-as-a-tor-onion-mirror-on-a-readable-168/", "url": "https://engineer.company/sv/portfolio/published-the-site-as-a-tor-onion-mirror-on-a-readable-168/", "title": "Publicerade webbplatsen som en onion‑spegel i Tor på en läsbar adress som börjar med engineer, med nyckeln genererad utanför värden så att den aldrig nådde detta repo eller en bärbar dator, och lät den förbli den enda av fyra ytterdörrar som aldrig räknas.", "summary": "Webbplatsen har fyra ytterdörrar till ett och samma bygge, och det här är den enda som aldrig räknas.", "content_html": "<p><strong>Situation.</strong> Webbplatsen svarade redan på tre protokoll från ett och samma bygge. En läsare som behövde nå den utan att röja att den hade nåtts gjorde fortfarande en rundtur via en utgångsnod till en vanlig adress, vilket är svagare än en egen ytterdörr. Den självklara invändningen mot en fjärde dörr är brandväggen, och den gäller inte: anonymitetsdaemonen når nätverket genom att bygga utgående kretsar, så ingenting ansluter inåt, ingen port öppnas och inget av brandväggsskikten har något att synkronisera.</p>\n<p><strong>Uppgift.</strong> Spegeln måste servera samma sidor som den vanliga adressen, publiceras överallt där de andra speglarna publiceras, bära en adress som en människa kan läsa högt i stället för en slumpmässig sträng, och aldrig låta den privata nyckel som är adressen röra det här repot eller en bärbar dator.</p>\n<p><strong>Handling.</strong> Rollen installerar daemonen från distributionens eget paket, skriver en konfiguration där den utgående proxyporten är avstängd och avstår från att skriva över en identitet som den inte själv har genererat — så en nyckel som placerats för hand används helt enkelt. En läsbar adress kan inte väljas, bara hittas: adressen är den publika nyckeln i base32, så ett prefix köps genom att generera nyckelpar tills ett av dem råkar koda de önskade bokstäverna, och varje tecken kostar trettiotvå gånger så mycket som det föregående. Prefixet som köptes läses som företagets eget namn. Adressen är ett deklarerat värde i inventory och varje konvergenskörning kontrollerar att värdens faktiska identitetsfil matchar det, så en utbytt nyckel med en inaktuell deklaration får körningen att fallera i stället för att tystna. Nyckeln fångas av båda spärrarna mot hemligheter, sedan det hade mätts upp att det självklara mönstret för en nyckelfil matchar en deploy‑nyckel och missar den här. Att pensionera den slumpmässiga adress som spegeln hade levererats med var en stegvis övergång i stället för en omkoppling i ett slag, så den publicerade adressen fortsatte att fungera medan den läsbara publicerades. Det steg som såg ut att vara det sista var det inte: nyckeln hamnade rätt och konvergenskörningen rapporterade ingen ändring, eftersom ingenting ännu namngav den nya katalogen, så den nya adressen publicerades aldrig alls — rollen fallerar nu på en identitet på disk som ingen tjänst namnger.</p>\n<p><strong>Resultat.</strong> Webbplatsen har fyra ytterdörrar till ett och samma bygge, och det här är den enda som aldrig räknas. Gemini‑förfrågningar och Gopher‑anslutningar räknas per dag utan adress och utan sökväg; onion‑spegeln har ingen räknare och ingen flagga för att lägga till en, eftersom det enda som det protokollet finns för att förhindra är just att en läsare räknas där, och blindheten är priset för hållningen snarare än en lucka i rapporteringen. Spegeln betalade också för sig själv som en andra bedömning: dess första läsare använde en annan webbläsarmotor, som inte implementerar den scrollstyrda animation som sidfoten förlitade sig på, så var och en av dem fick kolofonen målad tvärs över förstasidan. Det var ett verkligt fel på den vanliga webbplatsen, hittat av den publik som inte hade något annat sätt att rapportera det.</p>\n", "date_published": "2026-10-18T04:44:42+02:00", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv", "tags": [ "Infrastruktur", "Linux och servrar", "Systemadministration", "Säkerhet", "Webbutveckling", "Infrastruktur som kod", "Säkerhet och behörighetshantering", "Webbutveckling och CMS" ] }, { "id": "https://engineer.company/sv/notes/optimise-before-you-shard/", "url": "https://engineer.company/sv/notes/optimise-before-you-shard/", "title": "Optimera databasen innan du shardar den", "summary": "De flesta databaser som verkar behöva shardas behöver först läsas. Vår arbetsordning: mät, skärp, flytta bort läsningar, partitionera, sharda sedan.", "content_html": "<p>När en databas blir långsam under last är det första förslaget oftast fler\nmaskiner. Ibland är det rätt. Oftare är den långsamma delen en handfull satser\nsom ingen har läst ännu, och att sprida datan över flera servrar sprider de\nsatserna över mer hårdvara utan att göra någon av dem billigare. Ett problem som\nser olösligt ut är ofta bara en frågeplan från ett snyggt svar, och horisontell\nskalning väljs ibland just för att ingen ska behöva läsa\nden databas du redan har (läs den innan du köper mer av den).</p>\n<p>Det här är ordningen vi arbetar i innan vi delar upp något: titta, skärp, flytta\nläsningarna från primärnoden, partitionera tabellerna runt den heta punkten och\nsharda först när en maskin är full av bra arbete. Blixtrean, där varje köpare\nskriver till en enda rad, gås igenom i\n<a href=\"/sv/notes/selling-limited-stock-without-overselling/\">Att sälja den sista biljetten exakt en gång</a>.\nDen här anteckningen är metoden runt den, och dess sista avsnitt tar sig an det\nmotsatta förslaget: sälj från varje server och be om ursäkt för de enheter som\nsåldes två gånger.</p>\n<h2 id=\"titta-först\">Titta först</h2>\n\n<p>PostgreSQL registrerar redan det mesta som finns att veta. Ställ fyra frågor\ntill den innan du ändrar något.</p>\n<p><strong>Vilka satser kostar mest totalt?</strong> <code>pg_stat_statements</code>, ett tillägg som läses\nin via <code>shared_preload_libraries</code>, sparar varje normaliserad sats med antalet\nanrop och tiden. Sortera på total tid, inte på medelvärde: en SQL-fråga på 2 ms\nsom körs en miljon gånger i timmen kostar mer än en rapport på två sekunder som\nkörs två gånger.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">calls</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">total_exec_time</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">total_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">mean_exec_time</span><span class=\"p\">::</span><span class=\"nb\">numeric</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">mean_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_statements</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ORDER</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">total_exec_time</span><span class=\"w\"> </span><span class=\"k\">DESC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p><strong>Vad gör den översta faktiskt?</strong> <code>EXPLAIN (ANALYZE, BUFFERS)</code> kör satsen och\nvisar planen den valde, de rader varje steg verkligen returnerade mot de rader\nden väntade sig, och hur många sidor den läste. En skanning av en miljon rader\nför att returnera tio, eller en uppskattning som slår fel med en faktor hundra,\när oftast hela förklaringen. <code>ANALYZE</code> kör det den förklarar, så lägg en\nskrivning mellan <code>BEGIN</code> och <code>ROLLBACK</code>.</p>\n<p><strong>Vem väntar, och på vem?</strong></p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">pid</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">pg_blocking_pids</span><span class=\"p\">(</span><span class=\"n\">pid</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">blocked_by</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_activity</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s1\">'Lock'</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Rader här under en hektisk minut betyder att transaktioner köar för varandra,\ninte för hårdvaran, och en större maskin kortar inte en kö till en enda rad.</p>\n<p><strong>Arbetar servern eller jonglerar den?</strong> Räkna de aktiva sessionerna i\n<code>pg_stat_activity</code> mot antalet kärnor. När många gånger fler sessioner är\naktiva än det finns kärnor ägnar servern sin tid åt att växla mellan dem, och en\nanslutningspool löser det innan någon hårdvara gör det. Läs sedan <code>n_dead_tup</code>\ni <code>pg_stat_user_tables</code>: en tabell med fler döda rader än levande skannas genom\nsitt eget skräp, vilket är autovacuum som halkar efter, inte en databas som är\nför liten.</p>\n<p>Skriv ned vad du hittar. Det är utgångsläget som varje senare steg mäts mot, och\ndet ärliga svaret på ”behöver vi sharda?” finns ofta redan i det.</p>\n<h2 id=\"skärp-det-som-redan-finns\">Skärp det som redan finns</h2>\n\n<p>Ungefär i ordning efter vad det ger för insatsen.</p>\n<p><strong>Index som matchar frågorna.</strong> Planerna ovan pekar ut skanningarna. Ett index\npå de kolumner som en het SQL-fråga filtrerar på gör en skanning till ett\nuppslag, och ett partiellt index rymmer bara de rader en SQL-fråga kan vilja\nha — enheter som fortfarande är till salu, till exempel — så att det krymper\nmedan en rea pågår i stället för att växa:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">INDEX</span><span class=\"w\"> </span><span class=\"n\">units_unsold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ON</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">item_id</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Varje index betalas också vid varje skrivning, så ta bort dem vars <code>idx_scan</code> i\n<code>pg_stat_user_indexes</code> står kvar på noll.</p>\n<p><strong>Typer som säger vad värdet är.</strong> Scheman som skrivs snabbt, av en människa\neller en modell, tar till <code>TEXT</code> där en <code>SMALLINT</code> eller en <code>BOOLEAN</code> skulle\nräcka. En <code>BOOLEAN</code> är en byte, en <code>SMALLINT</code> två, och <code>'yes'</code> lagrat som text\när fyra. PostgreSQL lägger också in utfyllnad så att varje kolumn med fast bredd\nhamnar på sin egen justeringsgräns, så en kolumnordning som har valts av en\nslump kostar också byte. En lagerenhet, snabbt skriven:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\"> </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"c1\">-- 'yes' eller 'no'\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"c1\">-- 'yes' eller 'no'\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Och samma enhet med typer som säger vad värdena är, bredast först:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>På PostgreSQL 18 mäter den första raden 72 byte och den andra 54, en fjärdedel\nmindre, så mer av tabellen ryms i minnet. Typerna vägrar också värden som\n<code>'maybe'</code> som applikationen annars skulle behöva försvara sig mot, och ett\n<code>CHECK</code>-villkor gör samma sak för intervall: <code>quantity smallint CHECK (quantity > 0)</code>.</p>\n<p><strong>Färre nätverksrundor, och arbetet där datan finns.</strong> Varje sats som\napplikationen skickar kostar en nätverksrunda och en parsning. En loop som läser\nhundra rader och uppdaterar dem en i taget är hundraen nätverksrundor där en\nenda mängdbaserad sats skulle räcka, och en sida som hämtar varje enhet av en\nvara för att räkna de lediga skickar tusentals rader för att räkna ut ett enda\ntal. Regeln vi följer: det som måste se datan går till databasen — filtrering,\nräkning, en skrivning som beror på vad som finns där — och affärsregler stannar\ni applikationen, eftersom databasens CPU är den som är svårast att få mer av i\nhela systemet.</p>\n<p><strong>Uppdateringar som stannar på sin sida.</strong> En rad som ändras många gånger i\nminuten lämnar en ny version av sig själv efter sig varje gång. När en\nuppdatering inte ändrar någon indexerad kolumn och sidan har plats, behåller\nPostgreSQL den nya versionen på samma sida och rör inget index alls: en\nheap-only tuple, eller HOT-uppdatering. Håll därför de kolumner som en frekvent\nuppdatering ändrar utanför indexen, och lämna plats på sidorna:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">fillfactor</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">90</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Inställningen gäller sidor som skrivs från och med då, så sätt den innan raderna\nläses in. På tabellen ovan var 2 000 reservationer som satte <code>reserved</code>,\n<code>reserved_at</code> och <code>order_id</code> alla HOT, och av 2 000 uppdateringar som satte\n<code>sold</code>, som det partiella indexet läser, var ingen HOT. En försäljning sker en\ngång per enhet; en reservation kan ske många gånger. <code>n_tup_hot_upd</code> mot\n<code>n_tup_upd</code> i <code>pg_stat_user_tables</code> visar kvoten i ett system i drift.</p>\n<h2 id=\"en-rad-säger-vem-som-var-först\">En rad säger vem som var först</h2>\n\n<p>En enhet går till den första köpare vars sats når den, och reservationen löper\nut enligt databasens klocka. Två kolumner bär den: <code>reserved</code>, en boolean, och\n<code>reserved_at</code>, tidpunkten då den sattes.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">true</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">unit</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"o\"><</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En rad tillbaka: den här köparen har den. Ingen: någon annan var först, eller så\när den såld. Två reservationer av samma enhet köar på dess radlås, och med READ\nCOMMITTED, standardnivån i PostgreSQL, kontrollerar den andra <code>WHERE</code>-villkoret\nigen mot raden som den första committade, så att exakt en av dem får en rad\ntillbaka. En reservation som är äldre än tio minuter räknas som ledig, så\ningenting behöver städa bort den för att svaret ska bli rätt.</p>\n<p>Det gör booleanen ensam till ett ofullständigt svar. <code>reserved</code> förblir true\nefter att en reservation har löpt ut, och bara de två kolumnerna tillsammans\nsäger om en enhet är reserverad, så varje SQL-fråga som vill veta det måste\nfråga efter båda. En periodisk städning som nollställer utgångna reservationer\nhåller tabellen ärlig för den som läser <code>reserved</code> ensam, men korrektheten\nväntar aldrig på den. Ett villkor håller paret konsekvent:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">reserved_has_time</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Biljettanteckningen lagrar tidsgränsen, <code>held_until</code>, i stället för starten;\ndet är samma lösning. När köparen väljer enheten — en sittplats, ett numrerat\ntryck — namnger satsen den, som här. När vilken enhet som helst duger väljer\nden anteckningens <code>SKIP LOCKED</code>-form en som ingen annan håller.</p>\n<h2 id=\"flytta-läsningarna-från-primärnoden\">Flytta läsningarna från primärnoden</h2>\n\n<p>Det mesta av en butiks trafik är läsning: produktsidan, ”3 kvar”, varukorgen,\norderhistoriken. Inget av det behöver primärnoden, den enda maskin som måste\nhållas fri för skrivningar. En strömmande replik tar de läsningarna, och att\nlägga till en är konfiguration där en shard är ett projekt.</p>\n<p>Två förbehåll följer med. En replik ligger lite efter, så en köpare som just har\nreserverat en enhet och sedan läser sin varukorg från den kanske inte ser\nreservationen: läs det en användare just har skrivit från primärnoden, eller\nskicka den användaren till primärnoden i några sekunder efter varje skrivning.\nOch en lång rapport på en replik kan avbrytas när den håller upp ändringarna som\nrepliken spelar upp. PostgreSQL-dokumentationen kallar det frågekonflikter och\nlistar de inställningar som väger replikens eftersläpning mot dem.</p>\n<p>Den billigaste läsningen är en som aldrig når en databas. När varje enhet av en\nvara är reserverad kan ingen köpa den förrän en reservation löper ut eller\nsläpps, och databasen kan säga exakt när det sker:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"k\">count</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">FILTER</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"o\"><</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">free</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">min</span><span class=\"p\">(</span><span class=\"n\">reserved_at</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">until</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>När <code>free</code> är noll är <code>until</code> tidpunkten då den tidigaste reservationen löper\nut: cacha ”fullt reserverad” med den som utgångstid, aldrig längre, så avvisas\nvarje besökare fram till dess av cachen i stället för av databasen. När <code>free</code>\när noll och <code>until</code> är tom är varje enhet såld och svaret slutgiltigt. En\nreservation som släpps i förtid — ett nekat kort, en övergiven varukorg — tar\nbort cacheposten, så att enheten genast är till salu igen.</p>\n<p>Cachen säger aldrig något annat än nej. Ett ja kommer alltid från den villkorade\n<code>UPDATE</code> ovan, så en inaktuell cache kan kosta en försäljning i några sekunder\noch kan aldrig sälja en enhet två gånger. Det är biljettanteckningens cachade\n”slutsålt”, med en tidsgräns.</p>\n<h2 id=\"partitionera-tabellerna-runt-den-heta-raden\">Partitionera tabellerna runt den heta raden</h2>\n\n<p>Partitionering delar upp en tabell i flera efter en nyckel — en månad, en\ntenant — bakom ett enda namn. Det är värt att säga rakt ut vad den inte gör: den\ngör inte en omstridd rad något snabbare. Den raden finns fortfarande i en\npartition, på en maskin, bakom ett lås.</p>\n<p>Det den gör är att hålla tabellerna runt den heta punkten små. Ordrar,\nreservationshistorik och händelser växer för alltid, och de flesta SQL-frågor\nvill ha den senaste delen:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">created_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">quantity</span><span class=\"w\"> </span><span class=\"nb\">smallint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">quantity</span><span class=\"w\"> </span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">paid</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">id</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">created_at</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">RANGE</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">created_at</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders_2026_10</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">OF</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">VALUES</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">'2026-10-01'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">TO</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">'2026-11-01'</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En SQL-fråga som anger ett datumintervall läser bara partitionerna inom det, och\nden här månadens index förblir små nog att ligga i minnet. Gallring slutar\nvara en <code>DELETE</code> av miljontals rader, med den uppsvällning och vacuum-körning\nsom följer, och blir en enda sats:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">DETACH</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"n\">orders_2025_10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Primärnyckeln måste innehålla partitionsnyckeln, och därför är den\n<code>(id, created_at)</code>. En SQL-fråga som inte anger nyckeln läser varje partition,\nså partitionera på det som SQL-frågorna redan filtrerar på.</p>\n<h2 id=\"när-sharding-är-rimligt\">När sharding är rimligt</h2>\n\n<p>Det finns en gräns som sharding inte kan flytta: varje vara har en ägare. En\nenhet kan inte reserveras på två servrar samtidigt, så sharding flyttar hela\nvaror, aldrig halva. När en enda vara växer ur sin maskin delar\nbiljettanteckningens hinkar upp dess lagersaldo i mindre delar, och varje del\nhar återigen exakt en ägare.</p>\n<p>Inom den ramen kallar vi sharding rimligt när något av följande stämmer, och\ninte förr:</p>\n<ul>\n<li><strong>En maskin är full av bra arbete.</strong> Satserna är redan slimmade — en villkorad\nskrivning var, ingenting externt inuti en transaktion, uppdateringar som\nstannar på sin sida — och primärnodens CPU eller write-ahead-logg är ändå\nmättad, på den största maskin som är värd att köpa, över många varor snarare\nän en. En het vara är ett hinkproblem, inte ett shardproblem.</li>\n<li><strong>Datan har vuxit ur sitt underhåll.</strong> En säkerhetskopia kan inte återställas\ninom den tid som verksamheten har blivit lovad, ett index kan inte byggas\neller en tabell städas med vacuum under de lugna timmarna, eller så ryms\narbetsmängden inte längre i minnet.</li>\n<li><strong>Isolering är kravet.</strong> En tenant vars avtal eller tillsynsmyndighet säger\natt dess data ska ligga för sig, eller vars rea inte får dela disk med någon\nannans.</li>\n</ul>\n<p>Kostnaden hamnar på allt annat. Varje migrering, säkerhetskopiering,\nåterställning och rapport körs en gång per shard; en join över shards blir\napplikationskod; och en SQL-fråga som inte känner till sin shardnyckel frågar\nalla. Det är den nota som avsnitten ovan finns till för att undvika, och när den\nväl är betald är det svårt att ta sig tillbaka.</p>\n<h2 id=\"när-en-ursäkt-är-billigare-än-ett-lås\">När en ursäkt är billigare än ett lås</h2>\n\n<p>Det finns ett seriöst argument för att gå åt andra hållet. Låt varje server\nsälja av det lager den ser, acceptera att en enhet då och då går till två\nköpare, och låt kundtjänst återbetala en av dem med en ursäkt. Affärslogiken\nblir slutligt konsistent, byggd för de 99,9 % av begärandena som aldrig krockar,\noch en asynkron loop hittar krockarna i efterhand och gottgör dem. I gengäld\nbehöver butiken aldrig säga ”försök igen”. Helland och Campbell för fram den\nstarkaste formen av argumentet i <em>Building on Quicksand</em>: ett system som\nbekräftar arbete innan dess reservnod har fått det gör varje löfte\nprobabilistiskt, och företag har i alla tider gjort upp om löften de inte kunnat\nhålla genom att be om ursäkt.</p>\n<p>Vi håller med om det mesta av det. Vi är oense om var det ska tillämpas.</p>\n<p><strong>Det mesta av en butik bör vara slutligt konsistent.</strong> Katalogen, sökindexet,\nmärket med ”3 kvar”, varukorgen, rekommendationerna, e-postmeddelandena, flödet\ntill lagret och rapporterna kan alla ligga sekunder efter, och att låta dem\nvänta på primärnoden kostar snabbhet och köper ingenting. Dynamo byggdes för\njust detta: Amazons kundvagn glömmer eller avvisar aldrig ett ”lägg i\nkundvagnen”, inte ens när maskiner fallerar, och priset är att en sammanslagning\nkan återställa en vara som köparen hade tagit bort. Efter reservationen är\nbiljettanteckningens kassaflöde redan asynkront: en saga av lokala steg,\nmeddelanden som levereras minst en gång, och konsumenter som är idempotenta,\nnycklade på ordern. En avstämningsloop hör hemma i varje lösning som talar med\nen betalningsleverantör, eftersom webhooks går förlorade.</p>\n<p><strong>Reservationen är det enda ställe där det inte håller.</strong> ”Sällsynt” förutsätter\natt krockar är sällsynta, och i en blixtrea är krockarna själva rean. En server\nsom säljer utifrån sin egen bild fortsätter sälja tills den hör vad de andra\nsålde, så översäljningen blir ungefär försöksfrekvensen gånger\nreplikeringsfördröjningen. I en enkel simulering delar två regioner på\n2 000 försök i sekunden, var och en säljer utifrån sin egen räkning och hör om\nden andras försäljningar 200 millisekunder senare. Över fem körningar av en rea\npå 10 000 enheter sålde varje körning mellan 191 och 205 enheter som inte\nfanns. Räknestycket förutsäger 200: försöksfrekvensen, gånger fördröjningen,\ngånger andelen försäljningar som görs i den andra regionen. Med en ägare per\nvara sålde samma modell exakt lagret. Det händer varje gång något säljer slut,\noavsett om något gick sönder eller inte, och det inträffar vid klimax, när flest\nköpare tittar.</p>\n<p><strong>Upptiden är mindre än den låter.</strong> Gilbert och Lynch bevisade den\navvägning som slutlig konsistens gör: medan nätverket är partitionerat kan ett\nsystem svara eller vara konsistent, inte båda. Abadis PACELC lägger till den\nandra halvan. När ingenting är partitionerat är avvägningen latens mot\nkonsistens, och en ägare per vara, med slimmade satser, är redan snabb. Ingen av\nlösningarna når 100 %: betalningsleverantören, DNS och nätverken mellan dem har\nsina egna avbrott. Det den strikta lösningen ger upp är smalt. När ägaren av en\nvaras lagersaldo inte går att nå slutar den varan att säljas under de sekunder\nsom en failover tar, och biljettanteckningens avsnitt om att pausa varan\navgränsar pausen till varorna på maskinen som gick ned. Resten av butiken\nfortsätter sälja.</p>\n<p><strong>En ursäkt är inte en återbetalning.</strong> Återbetalningen är den billiga delen.\nKöparen fick ett ja, och kan redan ha bokat tåget, tagit ledigt och berättat för\nsina vänner. Att höra ”slutsålt” en sekund för tidigt är en besvikelse. Att höra\natt en bekräftad order har dragits tillbaka är ett svikit löfte. Kundtjänst är\nbemannad för en vanlig dag, och översäljningar kommer samtidigt, under årets\nmest hektiska minut. Innan du planerar att dra tillbaka bekräftade ordrar, fråga\nen jurist vad en bekräftelse förpliktar butiken till.</p>\n<p><strong>Behåll den asynkrona delen, och flytta den före jaet.</strong> Det som förslaget har\nrätt i är att köparen inte behöver vänta på ett lås. Ta emot varje klick i en\nbeständig, replikerad logg, en partition per vara, och säg till köparen att\nsvaret är på väg. Att ta emot ett klick beror då bara på loggen, och en logg som\nreplikeras över tre maskiner fortsätter ta emot skrivningar när en av dem går\nsönder. En ägare per vara läser loggen i ordning och avgör, i minnets\nhastighet, och svaret går tillbaka som ett meddelande inom några sekunder. När\nden ägaren gör failover väntar begärandena i loggen i stället för att avvisas\neller säljas två gånger. Jaet är en reservation, och betalningen följer på den\nsom i biljettanteckningens kassaflöde, så ingen betalar för en enhet utan att\nfå den. Avstämningen sker fortfarande, men före löftet i stället för efter det.</p>\n<p><strong>Dela lagret, inte räkningen.</strong> När verksamheten behöver att regioner säljer\nutan att fråga varandra, ge varje region en kvot av lagret och låt den sälja av\nsin egen kvot utan att fråga någon. En region vars kvot tar slut ber om mer, och\nmot slutet av rean slås kvoterna ihop till en ägare igen. Bailis med kolleger\nreder ut vilka regler servrar kan upprätthålla utan att tala med varandra, och\nett lagersaldo som inte får gå under noll är inte en av dem. Kvoter tar inte\nbort det samtalet; de flyttar det bort från den heta vägen, till det sällsynta\nögonblick då en kvot fylls på. Det är den slutligt konsistenta lösning som\nbehåller invarianten. Dess enda kostnad är svansen, där en köpare avvisas i en\nregion medan en annan region fortfarande har lager.</p>\n<p><strong>När en översäljning är rätt policy.</strong> Vissa varor kan tillverkas igen: ett\ntryck på beställning, en t-shirt som leverantören fortfarande har, en\nlicensnyckel. Att sälja förbi räkningen är då en restorder med ett ärligt datum,\noch slutlig konsistens är rätt lösning. Flygbolag överbokar med flit. De känner\ntill hur stor andel av passagerarna som uteblir, prissätter risken och\nkompenserar enligt en regel som är skriven i förväg. Det är testet. En\növersäljning är acceptabel när verksamheten valde dess storlek och dess\ngottgörelse före rean. Den är en defekt när den drabbar den som råkade klicka\nsist.</p>\n<p><strong>Om du väljer ursäkter, bygg dem som betalningar.</strong> Loopen som förslaget\nbeskriver är en kedja för återbetalningar, och den behöver det som allt som\nflyttar pengar behöver:</p>\n<ul>\n<li><strong>Ett id på varje försäljning,</strong> så att ett omförsök är samma försäljning, och\nen liggare över försäljningar där poster bara läggs till. Lagersaldot härleds\nur liggaren och redigeras aldrig.</li>\n<li><strong>En regel för vem som behåller enheten</strong> som du skulle kunna förklara för den\nköpare som förlorade. Ordna efter loggen, inte efter klockorna på olika\nservrar, där några millisekunders avvikelse skulle avgöra vem som får\nåterbetalningen.</li>\n<li><strong>Kompensationer som är idempotenta och loggade:</strong> avbryt, annullera eller\nåterbetala, meddela köparen. De är sagans ångra-steg, körda av en maskin.</li>\n<li><strong>En tidsgräns.</strong> Loopen måste hitta varje översäljning innan något skickas,\noch innan köparen hittar den.</li>\n<li><strong>En budget, mätt.</strong> Översäljningar per rea, mot det tal som verksamheten\nskrev under på, bredvid invarianten på samma instrumentpanel, med ett larm när\nbudgeten är förbrukad.</li>\n</ul>\n<p>Vårt svar är att vara slutligt konsistent överallt runt reservationen, strikt\nvid reservationen och asynkron i hur svaret når köparen. Bygg för det vanliga\nfallet, men i en blixtrea är krocken det vanliga fallet. Om verksamheten vill ha\nnågra översäljningar i utbyte mot något, gör det till ett tal som verksamheten\nhar valt, med en gottgörelse som är skriven före rean, och mät det.</p>\n<h2 id=\"vad-vi-skulle-säga-till-nästa-person\">Vad vi skulle säga till nästa person</h2>\n\n<ul>\n<li><strong>Läs innan du köper.</strong> <code>pg_stat_statements</code> och en enda <code>EXPLAIN</code> besvarar\nfler kapacitetsfrågor än en ny server gör.</li>\n<li><strong>Låt typerna säga vad datan är.</strong> En boolean som lagras som boolean är\nmindre, och den vägrar nonsens.</li>\n<li><strong>Skicka till databasen det som måste se datan, och inget annat.</strong> Filtrering\noch räkning hör hemma där. Affärsregler gör det inte.</li>\n<li><strong>Cachen säger nej; bara databasen säger ja.</strong> Då kostar en inaktuell cache\nsekunder, aldrig en enhet.</li>\n<li><strong>Partitionering är städning.</strong> Den håller tabellerna runt en het rad små och\ngör ingenting för raden.</li>\n<li><strong>Sharda när en maskin är full av bra arbete,</strong> inte när den är full av\nväntan.</li>\n<li><strong>Stäm av före jaet, inte efter.</strong> Allt runt reservationen kan vara slutligt\nkonsistent. Själva reservationen, gjord på det sättet, översäljer med\nförsöksfrekvensen gånger fördröjningen, vid varje rea.</li>\n</ul>\n<h2 id=\"källor\">Källor</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/pgstatstatements.html\">PostgreSQL: pg_stat_statements</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/using-explain.html\">PostgreSQL: att använda EXPLAIN</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/monitoring-stats.html\">PostgreSQL: det kumulativa statistiksystemet</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-numeric.html\">PostgreSQL: numeriska typer</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-character.html\">PostgreSQL: teckentyper</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-boolean.html\">PostgreSQL: typen boolean</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/indexes-partial.html\">PostgreSQL: partiella index</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/storage-hot.html\">PostgreSQL: heap-only tuples (HOT)</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/ddl-partitioning.html\">PostgreSQL: tabellpartitionering</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/hot-standby.html\">PostgreSQL: hot standby och frågekonflikter</a></li>\n<li><a href=\"https://arxiv.org/abs/0909.1788\">Pat Helland och Dave Campbell, Building on Quicksand</a></li>\n<li><a href=\"https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf\">Seth Gilbert och Nancy Lynch, Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services</a></li>\n<li><a href=\"https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf\">Daniel Abadi, Consistency Tradeoffs in Modern Distributed Database System Design</a></li>\n<li><a href=\"https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf\">DeCandia et al., Dynamo: Amazon’s Highly Available Key-value Store</a></li>\n<li><a href=\"https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf\">Hector Garcia-Molina och Kenneth Salem, Sagas</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n</ul>\n", "date_published": "2026-10-14T00:00:00Z", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv" }, { "id": "https://engineer.company/sv/notes/selling-limited-stock-without-overselling/", "url": "https://engineer.company/sv/notes/selling-limited-stock-without-overselling/", "title": "Att sälja den sista biljetten exakt en gång", "summary": "En blixtrea översäljer på en enda het rad, inte på en stor databas. Det som stoppar det: en villkorad skrivning, reservationer som löper ut och en saga.", "content_html": "<p>En blixtrea ställer tiotusen enheter framför en miljon människor på en gång. Det\nintressanta felet är inte att butiken går ned. Det är att butiken håller sig\nuppe och säljer tiotusenfyrtio. Instinkten som följer — databasen är för liten,\nsharda den — svarar på en fråga som ingen har ställt. Databasen var aldrig för\nstor. Varje köpare skrev till\nsamma rad (hela incidenten bor i en enda rad).</p>\n<p>Vi plockar här isär problemet innan det når ett projekt: var taket kommer ifrån,\nden enda sats som tar bort det mesta av det, och vad man bygger när en enda\ndatabas verkligen inte räcker.</p>\n<h2 id=\"en-rad-alla-köpare\">En rad, alla köpare</h2>\n\n<p>Tjugo miljoner rader utspridda över ett kluster är ett löst problem. En rad som\nvarje begäran måste ändra är det inte, och fler maskiner rör den inte, eftersom\nraden finns på en av dem. Distribuerade lagringssystem säger det rakt ut:\nDynamoDB ger en enskild partition ungefär 1 000 skrivenheter per sekund, och\nvarje skrivning till ett objekt hamnar på det objektets partition, hur många\nandra det än finns.</p>\n<p>Det finns ett teorem bakom formen. Bailis med kolleger kallar en invariant för\ninvariant-confluent när repliker kan ta emot skrivningar var för sig och ändå\nslås samman till ett giltigt tillstånd. <code>stock >= 0</code> är skolboksexemplet på en\nsom inte är det: två repliker som var och en ser en enhet kvar säljer den båda,\noch ingen sammanslagning kan ta tillbaka den andra försäljningen. Att aldrig\növersälja kräver samordning. Den enda frågan är hur mycket av den varje\nförsäljning betalar för.</p>\n<h2 id=\"talet-som-avgör\">Talet som avgör</h2>\n\n<p>Taket för en vara är ungefär hur många försäljningar ett lås bär, delat med hur\nlänge låset hålls:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">max försäljningar/s per vara ≈ försäljningar per lås ÷ låsets hålltid\n</span></span></code></pre></div><table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th>Vad som håller raden</th>\n\t\t\t\t\t<th>Tak per rad</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lås, anropa sedan betalning</td>\n\t\t\t\t\t<td>~2/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lås, kör sedan applogik</td>\n\t\t\t\t\t<td>~100–200/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>En villkorad <code>UPDATE</code></td>\n\t\t\t\t\t<td>~500–2 000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Batchar om 100 per commit</td>\n\t\t\t\t\t<td>~50 000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>En skrivare i minnet</td>\n\t\t\t\t\t<td>~100 000+/s</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>Det här är storleksordningar, inte benchmarkresultat; hårdvaran flyttar dem. Det\nden inte flyttar är formen. Det mesta av ett kort lås är commiten som väntar på\natt dess logg ska nå disken, och därför vinner batchning med två\nstorleksordningar: hundra försäljningar delar på en enda skrivning till disk.</p>\n<p>Det lämnar fyra spakar, och varje lösning nedan drar i en av dem:</p>\n<ol>\n<li><strong>Håll låset kortare tid</strong> — en sats, ingenting över nätverket inuti den.</li>\n<li><strong>Gör mer per lås</strong> — batcha nedräkningarna.</li>\n<li><strong>Ha fler lås</strong> — dela upp lagersaldot på flera nycklar.</li>\n<li><strong>Skicka färre begäranden till låset</strong> — avvisa de flesta köpare innan de\nnår det.</li>\n</ol>\n<p>Den sista betyder mer än det låter. Med en miljon köpare och tiotusen enheter\nmåste 990 000 begäranden misslyckas. Avvisningar är det mesta av trafiken, så\n”slutsålt” måste vara det billigaste svar systemet ger: ett väntrum framför\nförsäljningen, en grind som släpper in ungefär lika många köpare som det finns\nenheter, en <code>sold_out</code>-flagga som CDN:et serverar utan att fråga någon, och en\nhastighetsbegränsning per konto.</p>\n<h2 id=\"åtgärda-den-enda-databasen-först\">Åtgärda den enda databasen först</h2>\n\n<p>Versionen som kryper fram, eller översäljer, ser oftast ut så här:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">BEGIN</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- applikationen kontrollerar talet,\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- och anropar kanske en betalningsleverantör\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">new</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">COMMIT</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Låset är rätt. Det som det hålls över är fel: varje millisekund som\napplikationen tillbringar mellan <code>SELECT</code> och <code>UPDATE</code> är en millisekund som\nnästa köpare väntar. Utan <code>FOR UPDATE</code> är det värre. Två köpare läser 1, båda\nskriver 0, och en enhet har sålts två gånger.</p>\n<p>Lösningen gör kontrollen och nedräkningen till en enda sats:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">tenant_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">tenant</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">>=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- en rad tillbaka: reserverad. ingen: slutsåld.\n</span></span></span></code></pre></div><p>I PostgreSQL med READ COMMITTED väntar en andra köpare som når raden medan den\nförsta håller den, och utvärderar sedan <code>WHERE</code>-villkoret på nytt mot raden som\nden första committade. Om den försäljningen tog den sista enheten uppfylls inte\nvillkoret, och den andra köparen får ingen rad tillbaka — slutsålt — i stället\nför ett negativt tal. Med REPEATABLE READ eller SERIALIZABLE får den andra\nköparen i stället ett serialiseringsfel, vilket är säkert men gör konkurrensen\nom raden till omförsök; READ COMMITTED är den nivå som den här satsen är skriven\nför. InnoDB i MySQL kommer fram till samma svar, eftersom en <code>UPDATE</code> där läser\nden senast committade raden på alla nivåer. Lägg också till integritetsvillkoret,\nså att en bugg någon annanstans misslyckas högljutt i stället för att sälja\nlager som inte finns:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">available_not_negative</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">>=</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Därefter resten av arbetet med en enda databas, ungefär i ordning efter vad det\nger:</p>\n<ul>\n<li><strong>Inget externt inuti transaktionen.</strong> Inget betalningsanrop, ingen\nHTTP-begäran, inget publicerat meddelande. De hör hemma före transaktionen\neller efter den.</li>\n<li><strong>Rör den heta raden sist.</strong> Infoga ordern och allt annat först, så att låset\npå den omstridda raden bara lever under den sista satsen och commiten.</li>\n<li><strong>Ingen optimistisk låsning på en het räknare.</strong> En versionskolumn gör\nkonkurrens till omförsök, och under en rea är omförsöken själva lasten.</li>\n<li><strong>Servera läsningarna från något annat ställe.</strong> Produktsidans ”12 kvar” kan\nkomma från en replik eller en cache som ligger en sekund efter. Bara\nförsäljningen behöver primärnoden.</li>\n<li><strong>Ge lagret en egen databas,</strong> så att en försäljning aldrig köar bakom\nkatalogen för samma disk.</li>\n<li><strong>Poola anslutningarna.</strong> Hundratusen köpare ska vara några dussin\nanslutningar genom PgBouncer, inte tusentals backendprocesser som köar för\nett enda lås.</li>\n</ul>\n<p>Sittplatser har en annan form, eftersom varje enhet är unik, och de kräver en\nannan sats. Varje plats är en rad, och en köpare hoppar över de rader som en\nannan transaktion håller i stället för att köa bakom dem:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">held_by</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 min'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">event_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">event</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">sold_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"n\">held_until</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">SKIP</span><span class=\"w\"> </span><span class=\"n\">LOCKED</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En reservation som har löpt ut är helt enkelt tillgänglig igen, så det behövs\ningen städprocess för att svaret ska bli rätt. Och <code>now()</code> är databasens klocka,\nså applikationsservrar vars klockor går olika kan inte vara oense om när en\nreservation tog slut.</p>\n<p>För många butiker är det här avsnittet hela lösningen. Gör det före allt nedan,\neftersom sharding är en enkelriktad dörr: lätt att gå igenom, dyr att gå\ntillbaka genom.</p>\n<h2 id=\"reservera-först-betala-sedan\">Reservera först, betala sedan</h2>\n\n<p>En köpare som tar den sista enheten och sedan överger kortformuläret har inte\nköpt den. En försäljning är alltså inte en skrivning utan en kort livscykel:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">AVAILABLE → HELD, i minuter → SOLD\n</span></span><span class=\"line\"><span class=\"cl\"> → RELEASED\n</span></span></code></pre></div><ul>\n<li><strong>Reservationen nycklas på begäran, inte på försöket.</strong> Varje köp i kassan bär\nen idempotensnyckel, och reservationstabellen har\n<code>UNIQUE (tenant_id, idempotency_key)</code>, så att ett upprepat klick får tillbaka\nden reservation det redan har i stället för en andra enhet. En detalj i\nPostgreSQL biter här: <code>INSERT ... ON CONFLICT DO NOTHING RETURNING id</code>\nreturnerar ingenting när raden redan fanns, så ”ingen rad tillbaka” betyder\n”slå upp den”, inte ”misslyckades”.</li>\n<li><strong>Varje övergång är villkorad.</strong> Att bekräfta en försäljning flyttar en\nreservation från <code>held</code> till <code>sold</code> bara där den fortfarande är <code>held</code>, och\natt släppa en reservation lägger tillbaka enheten i lagersaldot bara om den\negna uppdateringen ändrade exakt en rad. Ett släpp som körs två gånger — t.ex.\ntimeouten och avbrytknappen — lämnar tillbaka enheten en gång.</li>\n<li><strong>Reservationer löper ut efter minuter.</strong> Fem till tio är vanligt. Förläng\nreservationen en gång när betalningen startar, så att ett långsamt kort inte\nförlorar den enhet det betalar för.</li>\n<li><strong>Begränsa antalet enheter per köpare vid reservationen,</strong> så att ett\nkonto inte kan hålla hela lagret medan det bestämmer sig.</li>\n</ul>\n<p>Under alltihop ligger en rad, den mest användbara i lösningen:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">available + held + sold = total\n</span></span></code></pre></div><p>Den gäller efter varje transaktion, en enda SQL-fråga kontrollerar den, och när\nden slutar gälla har något översålt eller tappat en enhet — innan en kund säger\ndet.</p>\n<h2 id=\"när-en-databas-inte-räcker\">När en databas inte räcker</h2>\n\n<p>Ibland räcker den verkligen inte: många tenants, många reor\nsamtidigt, flera regioner. Strukturen som skalar behåller en regel — varje vara\nhar exakt en ägare av sitt lagersaldo, en lagertjänst, och ingenting annat\nbestämmer — och shardar den tjänsten på <code>(tenant_id, item_id)</code>, så att en varas\nskrivningar aldrig spänner över två shards. De största tenanterna får egna\nceller, så att deras rea inte kan svälta ut någon annans. Bara reservationen\nmåste vara synkron; allt efter den kan vara ett meddelande.</p>\n<p>För den heta varan inuti en shard finns det fyra verktyg, och vart och ett drar\ni en av spakarna ovan.</p>\n<p><strong>En grind framför.</strong> En Redis-räknare, som räknas ned av ett Lua-skript så att\nkontrollen och nedräkningen blir ett enda steg, släpper in ungefär lika många\nköpare som det finns enheter och avvisar alla andra på mikrosekunder. Den är\ninte den som avgör. Redis replikerar asynkront, så efter en failover kan den nya\nprimärnoden ha glömt några insläpp och släppt igenom några för många.\nDatabasens villkorade <code>UPDATE</code> avgör fortfarande, och en avstämningsprocess\nåterställer grinden från databasens tal.</p>\n<p><strong>Dela upp lagersaldot.</strong> Tiotusen enheter kan vara hundra hinkar om hundra,\nvar och en sin egen rad eller sin egen shard, med varje köpare skickad till en\nav dem: hundra lås i stället för ett. Idén är gammal — O’Neils\nescrow-transaktioner 1986, sedan demarkationsprotokollet från Barbará och\nGarcia-Molina, sedan de begränsade räknarna från Balegas med kolleger — och dess\nenda verkliga kostnad är svansen. En köpares hink tar slut medan andra\nfortfarande har lager, så antingen prövar man en annan hink eller flyttar\nresterna, och mot slutet av en rea slår man ihop dem till en igen. Samma idé ger\nvarje region en egen kvot, så att de flesta försäljningar aldrig korsar ett hav\nför att fråga.</p>\n<p><strong>En skrivare per vara.</strong> Lägg varje begäran för en vara på en Kafka-partition,\nläst av en enda konsument som håller räkningen i minnet och committar sina\nbeslut i batchar. En tråd och inga lås: LMAX-formen. Begäranden bär ett id, och\nskrivaren kastar en som den redan har avgjort, eftersom en kö levererar två\ngånger.</p>\n<p><strong>Enheter som rader.</strong> När enheterna är få och unika — sittplatser, numrerade\nutgåvor — skapa en rad per enhet i förväg och ta dem med <code>SKIP LOCKED</code> som ovan,\neller håll enheternas id i en Redis-mängd och ta en med <code>SPOP</code>. Det finns ingen\nräknare kvar att slåss om.</p>\n<h2 id=\"resten-av-kassaflödet\">Resten av kassaflödet</h2>\n\n<p>Betalningen, ordern och kvittot är separata system, och ingen transaktion\nspänner över dem. Tvåfas-commit är skolboksvaret och fel svar här: den blockerar\nså länge någon deltagare är långsam, den lägger till fler nätverksrundor på den\nhetaste vägen, och betalningsleverantörer deltar inte i den. En saga gör jobbet,\nen sekvens av lokala steg, vart och ett med ett steg som ångrar det:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">1. reservera lager ångra: släpp reservationen\n</span></span><span class=\"line\"><span class=\"cl\">2. väntande order ångra: avbryt ordern\n</span></span><span class=\"line\"><span class=\"cl\">3. förläng reservationen (löper ut av sig själv)\n</span></span><span class=\"line\"><span class=\"cl\">4. auktorisera betalning ångra: annullera den\n</span></span><span class=\"line\"><span class=\"cl\">5. bekräfta HELD → SOLD\n</span></span><span class=\"line\"><span class=\"cl\">6. dra betalningen\n</span></span></code></pre></div><p>Att auktorisera först och dra betalningen sist betyder att en betalning som\nkommer fram efter att dess reservation har löpt ut annulleras i stället för att\nåterbetalas: pengarna flyttades aldrig. Varje steg skriver sitt meddelande till\nen outbox-tabell i samma transaktion som ändringen det meddelar, och change data\ncapture publicerar det, så att ingenting går förlorat mellan sparat och skickat.\nVarje konsument är idempotent, nycklad på ordern. Meddelanden kommer fram minst\nen gång, och idempotensen gör det i praktiken till en gång.</p>\n<p>Ett distribuerat lås hör inte hemma någonstans i det här. Kleppmanns poäng är\natt den som håller ett lås kan bli pausad längre än låsets lease-tid — en\nskräpsamling, en virtuell maskin som står still — och fortsätta skriva, och\nbotemedlet är en fencing-token som lagringssystemet kontrollerar. Men ett\nlagringssystem som kan kontrollera en token kan kontrollera <code>available >= :qty</code>,\noch då behövde det aldrig låset.</p>\n<h2 id=\"pausa-varan-översälj-den-aldrig\">Pausa varan, översälj den aldrig</h2>\n\n<p>Konsistens och tillgänglighet väljs per fråga, inte per system. ”Kan jag sälja\nden här enheten?” måste vara konsistent: när ägaren av en varas lagersaldo inte\ngår att nå slutar den varan att säljas. Allt annat — katalogen, märket med\n”3 kvar”, orderhistoriken — kan förbli tillgängligt och vara lite inaktuellt.</p>\n<p>Den tystaste översäljningen kommer från en failover. Med asynkron replikering\nbekräftar en primärnod en försäljning och dör innan dess replik har fått den;\nrepliken befordras, försäljningen är borta, och enheten säljs igen. Lösningen är\natt göra en försäljning beständig på mer än en maskin innan den bekräftas: en\nsynkron standby i PostgreSQL (<code>synchronous_commit</code> med\n<code>synchronous_standby_names</code>), eller ett lagringssystem byggt på Raft. Det kostar\nlatens vid varje commit, vilket förlänger precis den låshålltid som taket delas\nmed, och batchning och grinden köper tillbaka den. Avgränsa också pausen: en\nshard som har gått ned ska stoppa varorna på den, inte butiken.</p>\n<h2 id=\"bevisa-det\">Bevisa det</h2>\n\n<p>Inget av detta ska tas på tro, inte ens det vi säger.</p>\n<ul>\n<li><strong>Bevaka invarianten.</strong> Kör <code>available + held + sold = total</code> schemalagt, visa\nden i ett diagram och larma vid varje avvikelse. Noll översäljningar är ett\nservicenivåmål med ett larm, inte en förhoppning.</li>\n<li><strong>Framkalla kapplöpningen med flit.</strong> Tiotusen samtidiga köpare för hundra\nenheter, i testsviten, med kontroll av att exakt hundra såldes och att inget\ntal gick under noll.</li>\n<li><strong>Ha sönder saker under kapplöpningen.</strong> Döda primärnoden, kapa nätverket,\npausa en process — i Jepsen-stil — och kontrollera invarianten efteråt.</li>\n<li><strong>Lasttesta avvisningarna.</strong> Vägen för slutsålt måste förbli snabb när den är\n99 % av trafiken, och edge-nätverket måste ta upp den.</li>\n<li><strong>Samla mätvärdena på en instrumentpanel:</strong> låsväntetider, utgångna\nreservationer och avståndet mellan grindens räkning och databasens.</li>\n</ul>\n<h2 id=\"i-vilken-ordning\">I vilken ordning</h2>\n\n<ol>\n<li>Mät först. <code>pg_stat_statements</code> och låsväntetiderna visar om den heta raden\nalls är problemet.</li>\n<li>Gör skrivningen till en enda villkorad sats.</li>\n<li>Minska lasten: ett väntrum, en grind, ett cachat ”slutsålt”.</li>\n<li>Ge lagret en egen tjänst och en egen ägare.</li>\n<li>Sharda på <code>(tenant_id, item_id)</code>, och bara om de fyra första inte räckte.</li>\n<li>Lägg till ett läge för heta varor, hinkar eller en enda skrivare, för den rea\ndu vet kommer.</li>\n<li>Regioner sist, med kvoter.</li>\n</ol>\n<h2 id=\"vad-vi-skulle-säga-till-nästa-person\">Vad vi skulle säga till nästa person</h2>\n\n<ul>\n<li><strong>Sharding svarar på en annan fråga.</strong> Den sprider många rader, och en\növersåld rea är en enda rad. Ta reda på vilket du har innan du bygger.</li>\n<li><strong>Gör kontrollen och nedräkningen till en sats.</strong> Det mesta av avståndet\nmellan bra och översålt är tiden mellan att läsa ett tal och att skriva det.</li>\n<li><strong>Det mesta av trafiken är svaret nej.</strong> Gör avvisningen till det billigaste\nsystemet gör.</li>\n<li><strong>Pausa innan du översäljer.</strong> En vara som slutar säljas i trettio sekunder\nblir en ursäkt. En vara som säljs två gånger blir en återbetalning, en\nsupportkö och en kund som minns.</li>\n<li><strong>Skriv ned invarianten och bevaka den.</strong> Den är en rad, och den är det enda\nsom visar att lösningen fungerar i produktion snarare än i granskningen.</li>\n</ul>\n<h2 id=\"källor\">Källor</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED\">PostgreSQL: isoleringsnivån Read Committed</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE\">PostgreSQL: <code>FOR UPDATE</code> och <code>SKIP LOCKED</code></a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION\">PostgreSQL: synkron replikering</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n<li><a href=\"https://arxiv.org/abs/1503.09052\">Balegas et al., Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants</a></li>\n<li><a href=\"https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html\">Martin Kleppmann: How to do distributed locking</a></li>\n<li><a href=\"https://martinfowler.com/articles/lmax.html\">Martin Fowler: The LMAX Architecture</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html\">DynamoDB: utformning av partitionsnycklar</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html\">DynamoDB: partitioner och datafördelning</a></li>\n<li><a href=\"https://microservices.io/patterns/data/saga.html\">microservices.io: Saga</a></li>\n<li><a href=\"https://microservices.io/patterns/data/transactional-outbox.html\">microservices.io: Transactional outbox</a></li>\n<li><a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/replication/\">Redis: replikering</a></li>\n<li><a href=\"https://jepsen.io/\">Jepsen</a></li>\n</ul>\n", "date_published": "2026-10-13T00:00:00Z", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv" }, { "id": "https://engineer.company/sv/notes/safari-underline-font-subset/", "url": "https://engineer.company/sv/notes/safari-underline-font-subset/", "title": "Understrykningen som korsade varje bokstav", "summary": "Safari drog våra understrykningar rakt genom varje nedstapel. Orsaken var en kyrillisk typsnittsdelmängd på sidor som aldrig laddar den.", "content_html": "<p>Vår egen webbplats stryker under varje länk och låter webbläsaren skära ut en\nlucka där en bokstavs svans korsar linjen — <code>g</code>, <code>p</code>, kommatecknet. I Safari\ngjorde den inte det. Linjen gick rakt igenom dem. Så hade det varit på de\nengelska och danska sidorna så länge webbplatsen hade varit\nflerspråkig (buggen som ingen rapporterar, eftersom den bara ser billig ut).</p>\n<p>Orsaken var tre typsnittsdeklarationer för ett alfabet som de sidorna aldrig\nladdar ned.</p>\n<h2 id=\"vad-webbläsaren-ska-göra\">Vad webbläsaren ska göra</h2>\n\n<p><code>text-decoration-skip-ink</code> är egenskapen som lyfter en understrykning runt en\nnedstapel. Den är på som standard, och i åratal har svaret på ”min understrykning\nser fel ut” varit att ta till <code>text-underline-offset</code> och skjuta linjen längre\nned. Det är fel reflex: att flytta linjen ändrar designen, och luckan är det som\nfaktiskt saknades.</p>\n<p>Vi hittade två verkliga orsaker före den intressanta, och båda är värda att\nkänna till.</p>\n<ul>\n<li><strong>En understrykning på <code>1px</code> får en lucka i storleken <code>1px</code>.</strong> Brytningen som\nmotorn skär ut är proportionell mot tjockleken på linjen som skär. Vår\nstilmall låste tjockleken vid en pixel, vilket lämnade ungefär 0,6 px luft på\nvar sida om en stapel — tekniskt sett en lucka, visuellt en linje rakt genom\nbokstaven. Att sätta <code>text-decoration-thickness: auto</code> skalade båda\ntillsammans och tog luckan från 3,5 px till 12,4 px utan att flytta\nunderstrykningen en enda pixel.</li>\n<li><strong><code>from-font</code> är inte det säkra standardval det låter som.</strong> Det läser den\ntjocklek som typsnittet självt deklarerar, och Ubuntu deklarerar 0,056 em för\nLight, 0,020 em för Medium och 0,120 em för Bold — en familj vars linje i\nMedium är mindre än hälften så kraftig som den i Light. I stora storlekar blir\ndet en linje på sex pixlar bredvid en linje på två pixlar på samma sida.</li>\n</ul>\n<p>Det löste saken i Chrome. Safari drog fortfarande linjen genom allt.</p>\n<h2 id=\"den-del-som-tog-längst-tid\">Den del som tog längst tid</h2>\n\n<p>Vi mätte ordentligt: styr en riktig Safari, färga om understrykningen, ta en\nskärmbild i åtta gångers förstoring och räkna pixlarna där linjen slutar och\nbörjar.</p>\n<table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th></th>\n\t\t\t\t\t<th>ritad tjocklek</th>\n\t\t\t\t\t<th>lucka runt en nedstapel</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Chrome</td>\n\t\t\t\t\t<td>2,00 px</td>\n\t\t\t\t\t<td>5,8 / 12,5 / 6,3 px</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Safari</td>\n\t\t\t\t\t<td>1,38 px</td>\n\t\t\t\t\t<td>1,8 / 1,0 / 1,0 px</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>Safari skar alltså en lucka. Den var bara sex gånger smalare än den i Chrome,\noch i lässtorlek är en lucka på en pixel ingen lucka. Värre var att det vanliga\ngreppet gjorde saken värre: vid en tjocklek på 3 px gick Safari från tre luckor\ntill en, vid 4 px till ingen, eftersom den inte skalar luckan med linjen. Den\nlåser den vid ungefär 0,07 em i alla storlekar, där Chrome ger 0,6 em.</p>\n<p>Vi prövade ett tjugotal deklarationer mot det — <code>skip-ink: all</code>, båda äldre\nstavningarna av egenskapen, varje tjocklek från en halv pixel till fyra,\n<code>text-underline-position</code>, <code>font-smoothing</code>, <code>text-rendering</code>, <code>paint-order</code>.\nIngenting gjorde luckan bredare. Den ärliga slutsatsen vid den punkten var att\nWebKit helt enkelt satte ett tak för luckan och att det inte fanns mer att göra\nfrån en stilmall.</p>\n<p>Den slutsatsen var fel, och det som fällde den var\n<a href=\"https://github.com/fontsource/fontsource/issues/1096\">en buggrapport mot ett typsnittsprojekt</a>:\nSafari ignorerar skip-ink när ett typsnitt laddas med en icke-latinsk delmängd.</p>\n<h2 id=\"den-faktiska-orsaken\">Den faktiska orsaken</h2>\n\n<p>Webbplatsen serverar Ubuntu i sex delar — tre vikter med latinska tecken, tre\nmed kyrilliska — så att en ukrainsk sida sätts i samma typsnitt som en engelsk i\nstället för att falla tillbaka på vad systemet nu erbjuder. Varje del\ndeklarerar det teckenintervall den täcker, och webbläsaren laddar bara ned de\ndelar som en sida faktiskt behöver.</p>\n<p>Alla sex var deklarerade under ett och samma familjenamn, vilket är det\nsjälvklara sättet att skriva det. Och det är utlösaren, registrerad som\n<a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit-bugg 255159</a>: <strong>när ett\nsnitt i en familj deklarerar ett icke-latinskt teckenintervall försämrar Safari\nskip-ink för varje tecken i den familjen</strong> — även latinsk text på en sida som\naldrig hämtar den andra filen.</p>\n<p>En engelsk sida återgav sina understrykningar dåligt för att det fanns en\nukrainsk typsnittsdeklaration någonstans i stilmallen. Att ta bort de tre\nraderna under körning, och inte ändra något annat, tog luckorna från\n1,8/1,0/1,0 px till 4,5/11,0/5,0 px.</p>\n<h2 id=\"lösningen\">Lösningen</h2>\n\n<p>Ge den andra skriften ett eget familjenamn, och ange det i typsnittsstacken\nefter det första:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-css\" data-lang=\"css\"><span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"k\">font-face</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">font-family</span><span class=\"o\">:</span> <span class=\"nt\">UbuntuCyrillic</span><span class=\"o\">;</span> <span class=\"c\">/* var: Ubuntu */</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">src</span><span class=\"o\">:</span> <span class=\"nt\">url</span><span class=\"o\">(</span><span class=\"nt\">ubuntu_300_cyrillic</span><span class=\"p\">.</span><span class=\"nc\">woff2</span><span class=\"o\">)</span> <span class=\"nt\">format</span><span class=\"o\">(</span><span class=\"s1\">'woff2'</span><span class=\"o\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">unicode-range</span><span class=\"o\">:</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0400-045F</span><span class=\"o\">,</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0490-0491</span><span class=\"o\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">:</span><span class=\"nd\">root</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nv\">--body-font</span><span class=\"p\">:</span> <span class=\"n\">Ubuntu</span><span class=\"p\">,</span> <span class=\"n\">UbuntuCyrillic</span><span class=\"p\">,</span> <span class=\"n\">system-ui</span><span class=\"p\">,</span> <span class=\"kc\">sans-serif</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span></code></pre></div><p>En latinsk bokstav hittas i den första familjen och når aldrig den andra. En\nkyrillisk faller förbi den första, som saknar en sådan glyf, och landar i den\nandra i stället för i ett systemtypsnitt. Teckenintervallen avgör fortfarande\nvad som laddas ned, så ingenting i laddningen ändras: engelska och danska sidor\nhämtar tre latinska filer och inga kyrilliska, ukrainska sidor det omvända.</p>\n<p>Tre omdöpta deklarationer och en ny post i stacken. Luckorna i Safari blev\n4,5/11,0/5,0 px, Chrome lämnades orört, understrykningen flyttade sig inte, och\nukrainska sätts fortfarande i Ubuntu med identiska bredder.</p>\n<h2 id=\"vad-vi-skulle-säga-till-nästa-person\">Vad vi skulle säga till nästa person</h2>\n\n<ul>\n<li><strong>En skip-ink-mätning gäller bara för den motor som gav den.</strong> Vi hade en\ntabell med siffror, en korrekt diagnos och en verklig lösning, och allt var\nChromium. Skärmbilden som öppnade fallet igen kom från en människa som tittade\npå sidan.</li>\n<li><strong>Ta till tjockleken före förskjutningen.</strong> Att bredda luckan behåller\ndesignen; att flytta ned linjen ersätter den.</li>\n<li><strong>Att dela upp ett typsnitt efter skrift är rätt, men uppdelningen hör hemma\ni familjenamnet också.</strong> Ett gemensamt namn för flera skrifter läser bättre och\nkostar dig skip-ink i Safari. Att slå ihop dem igen ser ut som städning och är\nen regression.</li>\n<li><strong>Laddningen var aldrig problemet.</strong> Varje instinkt säger att en sida som\nåterges dåligt på grund av ett kyrilliskt typsnitt måste ladda ned något den\ninte borde. Det gjorde den inte. Deklarationen räckte i sig.</li>\n</ul>\n<h2 id=\"källor\">Källor</h2>\n\n<ul>\n<li><a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit-bugg 255159 — <code>text-decoration-skip-ink</code> och typsnittsdelmängder</a></li>\n<li><a href=\"https://github.com/fontsource/fontsource/issues/1096\">Fontsource-ärende 1096, som pekade ut utlösaren</a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-skip-ink\">MDN: <code>text-decoration-skip-ink</code></a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range\">MDN: <code>unicode-range</code></a></li>\n</ul>\n", "date_published": "2026-09-28T00:00:00Z", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv" }, { "id": "https://engineer.company/sv/notes/solana-mobile-wallet-deeplinks/", "url": "https://engineer.company/sv/notes/solana-mobile-wallet-deeplinks/", "title": "Öppna en dApp i Solanas mobilplånböcker", "summary": "Varför browse-djuplänkar till Phantom, Solflare och Backpack misslyckas i mobilen, och de format, regler och lösningar som får dem att fungera.", "content_html": "<p>En React-dApp byggd på <code>@solana/wallet-adapter-react</code> ansluter till plånböcker på\ndatorn utan problem, men i en telefon faller samma flöde isär: plånboken måste\nöppna dAppen i sin egen inbyggda webbläsare, och djuplänkarna som ska få det att\nhända gör det i tysthet inte. Backpack hamnar på en sida med ”ladda ned appen”;\nSolflare öppnar appen men aldrig webbplatsen; varje variant verkar misslyckas. Vi\nplockade isär problemet, och det visade sig vara fyra separata problem bakom ett\noch samma symptom.</p>\n<h2 id=\"de-fyra-problemen\">De fyra problemen</h2>\n\n<ul>\n<li><strong>Backpack-länken var felaktigt utformad.</strong> Det enda dokumenterade formatet är\n<code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code> — en universell länk med\nmål-URL:en i sökvägen och en obligatorisk <code>ref</code>. En gissning med ett eget\nURL-schema, som <code>backpack://ul/v1/browse?url=...</code>, matchar ingen rutt som\nappen registrerar, så användaren hamnar på plånbokens installationssida.</li>\n<li><strong>Solflare behöver också sin universella länk:</strong>\n<code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code>, inte det nakna schemat\n<code>solflare://</code>. Ett naket schema kan starta appen utan att dirigera den vidare —\nvilket är exakt ”appen öppnas, men webbplatsfliken måste öppnas för hand”.</li>\n<li><strong>Båda parametrarna måste kodas.</strong> <code>url</code> är dAppens fullständiga, absoluta\nadress och <code>ref</code> är ursprunget som gör begäran, och båda skickas genom\n<code>encodeURIComponent</code>. Ett okodat <code>?</code> eller <code>&</code> i målet förstör tolkningen, och\nplånboken öppnas på sin startskärm i stället för i webbläsarfliken.</li>\n<li><strong>Hur länken utlöses spelar lika stor roll som länken.</strong> Universella länkar\nbyter bara app vid en navigering som operativsystemet litar på — och de gör\navsiktligt ingenting när de klistras in i adressfältet, vilket också är hur en\nhelt korrekt länk ”misslyckas” under testning.</li>\n</ul>\n<h2 id=\"de-dokumenterade-formaten\">De dokumenterade formaten</h2>\n\n<ul>\n<li>Phantom: <code>https://phantom.app/ul/browse/<url>?ref=<ref></code> — utan <code>/v1</code> i den\nhär.</li>\n<li>Solflare: <code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code></li>\n<li>Backpack: <code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code></li>\n</ul>\n<p>Ett och samma mönster fungerar för alla tre:</p>\n<pre tabindex=\"0\"><code>const WALLET_BROWSE = {\n phantom: (url, ref) =>\n `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n solflare: (url, ref) =>\n `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n backpack: (url, ref) =>\n `https://backpack.app/ul/v1/browse/${url}?ref=${ref}`,\n};\n\nfunction walletBrowseLink(\n walletName,\n targetUrl = window.location.href,\n) {\n const build = WALLET_BROWSE[walletName.toLowerCase()];\n if (!build) return null;\n return build(\n encodeURIComponent(targetUrl),\n encodeURIComponent(window.location.origin),\n );\n}\n</code></pre><h2 id=\"utlös-länken-så-att-ios-och-android-accepterar-den\">Utlös länken så att iOS och Android accepterar den</h2>\n\n<ul>\n<li><strong>Rendera ett riktigt ankarelement, beräknat i förväg.</strong> En vanlig\n<code><a href={walletBrowseLink('phantom')}></code> är den mest pålitliga utlösaren på\nbåda plattformarna.</li>\n<li><strong>Om det måste ske programmatiskt</strong>, tilldela <code>window.location.href</code> synkront\ninuti hanteraren för trycket — ingen <code>await</code>, ingen <code>fetch</code>, ingen\n<code>setTimeout</code> först. Efter asynkront arbete är gestens kontext borta, och iOS\nfaller tillbaka på plånbokens webbplats. Aldrig <code>window.open</code>.</li>\n<li><strong>Testa aldrig genom att klistra in i adressfältet.</strong> Universella länkar\nutlöses avsiktligt inte där; testa med en länk som du trycker på eller en\nQR-kod som skannas med kameran.</li>\n<li><strong>Se upp med meddelandeappars webbvyer.</strong> I Telegrams eller Instagrams\ninbyggda webbläsare sväljs universella länkar ofta, och plånbokens vanliga\nwebbplats laddas i stället. Att känna igen dem på user agent är i bästa fall\nen heuristik, så ge också användarna en synlig nödutgång: ”öppna i Safari\neller Chrome och anslut sedan”.</li>\n</ul>\n<h2 id=\"den-större-lösningen-på-android\">Den större lösningen på Android</h2>\n\n<p>Handbyggda djuplänkar är iOS-sidan av saken. På Android låter Mobile Wallet\nAdapter från Solana Mobile en dApp som körs i mobilens webbläsare ansluta\ndirekt till den installerade plånboksappen, helt utan omvägen via en inbyggd\nwebbläsare. Nyare versioner av <code>@solana/wallet-adapter-react</code> registrerar\nmobiladaptern automatiskt, så en uppgradering av wallet-adapter-paketen kan på\negen hand lösa Android. Målarkitekturen: Mobile Wallet Adapter på Android,\nuniversella browse-länkar på iOS, där Apple inte tillåter någon motsvarighet.</p>\n<h2 id=\"verifiera-på-en-enhet\">Verifiera på en enhet</h2>\n\n<ol>\n<li>Riktig enhet, plånboken installerad, länken öppnad från systemets webbläsare\n— inte från en meddelandeapp.</li>\n<li>Tryck på en renderad länk eller skanna en QR-kod; klistra aldrig in i\nadressfältet.</li>\n<li>Bekräfta att plånboken öppnas och att dAppen laddas i dess inbyggda\nwebbläsarflik — den andra halvan är den del som misslyckas.</li>\n<li>Upprepa utan plånboken installerad: den universella länken ska då falla\ntillbaka på plånbokens webbplats. Visas den sidan medan appen är installerad\när länken eller utlösningen fortfarande fel.</li>\n<li>Testa sedan vägen via meddelandeappar, och lägg till tipset ”öppna i\nwebbläsaren” om det misslyckas där.</li>\n</ol>\n<h2 id=\"källor\">Källor</h2>\n\n<ul>\n<li><a href=\"https://docs.phantom.com/phantom-deeplinks/deeplinks-ios-and-android\">Phantom: djuplänkar på iOS och Android</a></li>\n<li><a href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\">Solflare: Browse-djuplänken</a></li>\n<li><a href=\"https://docs.backpack.app/deeplinks/other-methods/browse\">Backpack: Browse-djuplänken</a></li>\n<li><a href=\"https://docs.solanamobile.com/mobile-wallet-adapter/mobile-apps\">Solana Mobile: Mobile Wallet Adapter</a></li>\n</ul>\n", "date_published": "2026-08-10T00:00:00Z", "date_modified": "2026-10-18T04:44:42+02:00", "language": "sv" } ] }