[{"data":1,"prerenderedAt":908},["ShallowReactive",2],{"blog:list":3},[4,195,629],{"id":5,"title":6,"body":7,"date":180,"description":181,"draft":182,"extension":183,"meta":184,"navigation":185,"path":186,"seo":187,"stem":188,"tags":189,"__hash__":194},"blog\u002Fblog\u002Ffrom-xampp-to-ddev.md","From XAMPP to DDEV: Modernizing PHP Developer Environments",{"type":8,"value":9,"toc":171},"minimark",[10,14,22,27,35,38,42,56,59,87,91,94,123,127,138,147,151],[11,12,13],"p",{},"Setting up a PHP development environment used to be a rite of passage — and not the fun kind.",[11,15,16,17,21],{},"When I joined the team, the local dev setup had grown organically over the years — XAMPP on Windows, manual SSL certificates, environment variables scattered across ",[18,19,20],"code",{},".htaccess"," files. Six developers, all with subtly different configurations.",[23,24,26],"h2",{"id":25},"the-problem","The problem",[11,28,29,30,34],{},"The worst part wasn't the setup itself — it was ",[31,32,33],"strong",{},"onboarding",". Getting a new developer productive took 1–2 full days of hand-holding: installing the right PHP version, configuring Apache virtual hosts, mapping drive letters, fighting with Xdebug config.",[11,36,37],{},"Every developer had a slightly different environment, which meant bugs that were genuinely environment-specific.",[23,39,41],{"id":40},"the-solution-wsl-ddev-docker","The solution: WSL + DDEV + Docker",[11,43,44,51,52,55],{},[45,46,50],"a",{"href":47,"rel":48},"https:\u002F\u002Fddev.com\u002F",[49],"nofollow","DDEV"," is an open-source local development environment tool built on Docker. It gives every project a reproducible, containerised environment defined entirely in ",[18,53,54],{},".ddev\u002Fconfig.yaml"," — committed to the repo.",[11,57,58],{},"Combined with WSL2 (Windows Subsystem for Linux), we got:",[60,61,62,69,75,81],"ul",{},[63,64,65,68],"li",{},[31,66,67],{},"Consistent environments"," — the same Docker image on every machine",[63,70,71,74],{},[31,72,73],{},"Per-project PHP versions"," — no more global PHP installation conflicts",[63,76,77,80],{},[31,78,79],{},"Built-in Xdebug"," — one command to enable\u002Fdisable",[63,82,83,86],{},[31,84,85],{},"Mailpit"," — local email catching out of the box",[23,88,90],{"id":89},"the-migration","The migration",[11,92,93],{},"The migration itself took about a week:",[95,96,97,100,106,109,120],"ol",{},[63,98,99],{},"Documented the existing setup (PHP version, Apache vhosts, env vars, Xdebug config)",[63,101,102,103,105],{},"Created a ",[18,104,54],{}," translating every piece of that config",[63,107,108],{},"Tested against the existing codebase end-to-end",[63,110,111,112,115,116,119],{},"Wrote a setup guide (a single ",[18,113,114],{},"README"," section: install WSL2, install Docker Desktop, install DDEV, run ",[18,117,118],{},"ddev start",")",[63,121,122],{},"Ran a 2-hour workshop with the team",[23,124,126],{"id":125},"results","Results",[11,128,129,130,133,134,137],{},"Onboarding time dropped from ",[31,131,132],{},"1–2 days"," to ",[31,135,136],{},"1–2 hours",". The environment is now fully reproducible: any developer can check out the repo and have a working local setup with one command.",[11,139,140,141,146],{},"I also contributed a bug fix back to the DDEV project (",[45,142,145],{"href":143,"rel":144},"https:\u002F\u002Fgithub.com\u002Fddev\u002Fddev\u002Fpull\u002F6809",[49],"PR #6809",") while doing the migration — a small way to give back to the tool that saved us hours.",[23,148,150],{"id":149},"takeaways","Takeaways",[60,152,153,156,163],{},[63,154,155],{},"The upfront investment in a reproducible environment pays off immediately at hiring time",[63,157,158,159,162],{},"DDEV's ",[18,160,161],{},".ddev\u002F"," directory in version control is the key — no more \"ask the guy who set it up originally\"",[63,164,165,166,170],{},"Running a short workshop beats documentation alone; people need to ",[167,168,169],"em",{},"do"," the setup once with someone watching",{"title":172,"searchDepth":173,"depth":173,"links":174},"",2,[175,176,177,178,179],{"id":25,"depth":173,"text":26},{"id":40,"depth":173,"text":41},{"id":89,"depth":173,"text":90},{"id":125,"depth":173,"text":126},{"id":149,"depth":173,"text":150},"2026-03-10","How switching from a manual Windows\u002FXAMPP setup to WSL + DDEV + Docker cut onboarding time from 2 days to 2 hours — and what I learned doing it for a team of 6.",false,"md",{},true,"\u002Fblog\u002Ffrom-xampp-to-ddev",{"title":6,"description":181},"blog\u002Ffrom-xampp-to-ddev",[190,50,191,192,193],"PHP","Docker","DevOps","Developer Experience","CD7n9k8ACclQuLMOvkZx02zm6vnsvOcBkEnGD8SyuJs",{"id":196,"title":197,"body":198,"date":618,"description":619,"draft":182,"extension":183,"meta":620,"navigation":185,"path":621,"seo":622,"stem":623,"tags":624,"__hash__":628},"blog\u002Fblog\u002Fauto-generating-typescript-api-client.md","Auto-generating a TypeScript API Client from OpenAPI with @hey-api\u002Fopenapi-ts",{"type":8,"value":199,"toc":611},[200,203,210,214,221,383,386,393,397,407,422,469,472,557,561,567,571,574,590,593,597,604,607],[11,201,202],{},"One of the most common sources of bugs in full-stack apps is the frontend and backend diverging silently.\nYou rename a field on the API, forget to update the TypeScript interface, and two weeks later you get a production bug report.",[11,204,205,206,209],{},"At work, we built a new PHP\u002FLaminas backend alongside a Nuxt 3 frontend. From day one, I wanted to make the contract between them ",[167,207,208],{},"machine-enforced",".",[23,211,213],{"id":212},"the-setup","The setup",[11,215,216,217,220],{},"The backend uses ",[31,218,219],{},"OpenAPI attributes"," on every handler and DTO:",[222,223,227],"pre",{"className":224,"code":225,"language":226,"meta":172,"style":172},"language-php shiki shiki-themes one-dark-pro","\u003C?php\n\n#[OA\\Get(\n    path: '\u002Fcustomers',\n    operationId: 'getCustomers',\n    responses: [\n        new OA\\Response(response: 200, description: 'List of customers', content: new OA\\JsonContent(ref: CustomerListResponse::class))\n    ]\n)]\nclass GetCustomersHandler\n{\n    \u002F\u002F ...\n}\n","php",[18,228,229,246,251,257,270,281,287,344,350,356,364,370,377],{"__ignoreMap":172},[230,231,234,238,242],"span",{"class":232,"line":233},"line",1,[230,235,237],{"class":236},"sjrmR","\u003C",[230,239,241],{"class":240},"seHd6","?",[230,243,245],{"class":244},"sn6KH","php\n",[230,247,248],{"class":232,"line":173},[230,249,250],{"emptyLinePlaceholder":185},"\n",[230,252,254],{"class":232,"line":253},3,[230,255,256],{"class":244},"#[OA\\Get(\n",[230,258,260,263,267],{"class":232,"line":259},4,[230,261,262],{"class":244},"    path: ",[230,264,266],{"class":265},"subq3","'\u002Fcustomers'",[230,268,269],{"class":244},",\n",[230,271,273,276,279],{"class":232,"line":272},5,[230,274,275],{"class":244},"    operationId: ",[230,277,278],{"class":265},"'getCustomers'",[230,280,269],{"class":244},[230,282,284],{"class":232,"line":283},6,[230,285,286],{"class":244},"    responses: [\n",[230,288,290,293,296,300,303,307,310,313,316,318,321,324,326,329,332,335,338,341],{"class":232,"line":289},7,[230,291,292],{"class":240},"        new",[230,294,295],{"class":244}," OA\\",[230,297,299],{"class":298},"sU0A5","Response",[230,301,302],{"class":244},"(response:",[230,304,306],{"class":305},"sVC51"," 200",[230,308,309],{"class":244},",",[230,311,312],{"class":244}," description:",[230,314,315],{"class":265}," 'List of customers'",[230,317,309],{"class":244},[230,319,320],{"class":244}," content:",[230,322,323],{"class":240}," new",[230,325,295],{"class":244},[230,327,328],{"class":298},"JsonContent",[230,330,331],{"class":244},"(ref:",[230,333,334],{"class":298}," CustomerListResponse",[230,336,337],{"class":244},"::",[230,339,340],{"class":240},"class",[230,342,343],{"class":244},"))\n",[230,345,347],{"class":232,"line":346},8,[230,348,349],{"class":244},"    ]\n",[230,351,353],{"class":232,"line":352},9,[230,354,355],{"class":244},")]\n",[230,357,359,361],{"class":232,"line":358},10,[230,360,340],{"class":240},[230,362,363],{"class":298}," GetCustomersHandler\n",[230,365,367],{"class":232,"line":366},11,[230,368,369],{"class":244},"{\n",[230,371,373],{"class":232,"line":372},12,[230,374,376],{"class":375},"sV9Aq","    \u002F\u002F ...\n",[230,378,380],{"class":232,"line":379},13,[230,381,382],{"class":244},"}\n",[11,384,385],{},"A PHPStan rule enforces that every handler has OpenAPI documentation — missing it fails CI.",[11,387,388,389,392],{},"From those attributes, we generate an ",[18,390,391],{},"openapi.json"," spec file. That spec is the single source of truth.",[23,394,396],{"id":395},"generating-the-typescript-client","Generating the TypeScript client",[11,398,399,406],{},[45,400,403],{"href":401,"rel":402},"https:\u002F\u002Fheyapi.dev\u002F",[49],[18,404,405],{},"@hey-api\u002Fopenapi-ts"," reads the OpenAPI spec and generates:",[60,408,409,412,419],{},[63,410,411],{},"Typed request\u002Fresponse interfaces",[63,413,414,415,418],{},"A fully typed client (we use the ",[18,416,417],{},"fetch"," client)",[63,420,421],{},"SDK methods for every operation",[222,423,427],{"className":424,"code":425,"language":426,"meta":172,"style":172},"language-bash shiki shiki-themes one-dark-pro","npx @hey-api\u002Fopenapi-ts \\\n  --input .\u002Fopenapi.json \\\n  --output .\u002Fsrc\u002Fclient \\\n  --client @hey-api\u002Fclient-fetch\n","bash",[18,428,429,441,451,461],{"__ignoreMap":172},[230,430,431,435,438],{"class":232,"line":233},[230,432,434],{"class":433},"sVbv2","npx",[230,436,437],{"class":265}," @hey-api\u002Fopenapi-ts",[230,439,440],{"class":236}," \\\n",[230,442,443,446,449],{"class":232,"line":173},[230,444,445],{"class":305},"  --input",[230,447,448],{"class":265}," .\u002Fopenapi.json",[230,450,440],{"class":236},[230,452,453,456,459],{"class":232,"line":253},[230,454,455],{"class":305},"  --output",[230,457,458],{"class":265}," .\u002Fsrc\u002Fclient",[230,460,440],{"class":236},[230,462,463,466],{"class":232,"line":259},[230,464,465],{"class":305},"  --client",[230,467,468],{"class":265}," @hey-api\u002Fclient-fetch\n",[11,470,471],{},"The generated client looks like this on the consuming side:",[222,473,477],{"className":474,"code":475,"language":476,"meta":172,"style":172},"language-typescript shiki shiki-themes one-dark-pro","import { getCustomers } from '@acme\u002Fapi-client'\n\nconst { data, error } = await getCustomers({ query: { page: 1 } })\n\u002F\u002F data is fully typed — CustomerListResponse\n","typescript",[18,478,479,500,504,552],{"__ignoreMap":172},[230,480,481,484,487,491,494,497],{"class":232,"line":233},[230,482,483],{"class":240},"import",[230,485,486],{"class":244}," { ",[230,488,490],{"class":489},"sVyAn","getCustomers",[230,492,493],{"class":244}," } ",[230,495,496],{"class":240},"from",[230,498,499],{"class":265}," '@acme\u002Fapi-client'\n",[230,501,502],{"class":232,"line":173},[230,503,250],{"emptyLinePlaceholder":185},[230,505,506,509,511,514,517,520,522,525,528,531,534,537,540,543,546,549],{"class":232,"line":253},[230,507,508],{"class":240},"const",[230,510,486],{"class":244},[230,512,513],{"class":298},"data",[230,515,516],{"class":244},", ",[230,518,519],{"class":298},"error",[230,521,493],{"class":244},[230,523,524],{"class":236},"=",[230,526,527],{"class":240}," await",[230,529,530],{"class":433}," getCustomers",[230,532,533],{"class":244},"({ ",[230,535,536],{"class":489},"query",[230,538,539],{"class":244},": { ",[230,541,542],{"class":489},"page",[230,544,545],{"class":244},": ",[230,547,548],{"class":305},"1",[230,550,551],{"class":244}," } })\n",[230,553,554],{"class":232,"line":259},[230,555,556],{"class":375},"\u002F\u002F data is fully typed — CustomerListResponse\n",[23,558,560],{"id":559},"publishing-to-a-private-registry","Publishing to a private registry",[11,562,563,564,209],{},"We publish the generated client as a private npm package. The frontend installs it like any other dependency: ",[18,565,566],{},"npm install @acme\u002Fapi-client",[23,568,570],{"id":569},"keeping-it-up-to-date-github-actions","Keeping it up to date: GitHub Actions",[11,572,573],{},"The real value comes from automation. A GitHub Actions workflow:",[95,575,576,581,584,587],{},[63,577,578,579],{},"Runs on every push that touches PHP files or ",[18,580,391],{},[63,582,583],{},"Regenerates the client",[63,585,586],{},"Bumps the patch version",[63,588,589],{},"Publishes the new package to the registry",[11,591,592],{},"The frontend's Renovate config picks up the new version automatically and opens a PR.",[23,594,596],{"id":595},"the-result","The result",[11,598,599,600,603],{},"The gap between API contract and frontend types is now ",[31,601,602],{},"zero"," — they're the same file. Schema drift is impossible without a failing CI job. And new developers don't need to figure out the API shape by reading PHP code; they just look at the TypeScript types.",[11,605,606],{},"This pattern works equally well with any typed backend that can emit an OpenAPI spec.",[608,609,610],"style",{},"html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html pre.shiki code .sV9Aq, html code.shiki .sV9Aq{--shiki-default:#7F848E;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}",{"title":172,"searchDepth":173,"depth":173,"links":612},[613,614,615,616,617],{"id":212,"depth":173,"text":213},{"id":395,"depth":173,"text":396},{"id":559,"depth":173,"text":560},{"id":569,"depth":173,"text":570},{"id":595,"depth":173,"text":596},"2026-02-14","How I replaced hand-maintained API types with an auto-generated, fully typed TypeScript client — published to a private registry and rebuilt on every API spec change via GitHub Actions.",{},"\u002Fblog\u002Fauto-generating-typescript-api-client",{"title":197,"description":619},"blog\u002Fauto-generating-typescript-api-client",[625,626,627,193,190],"TypeScript","OpenAPI","GitHub Actions","z_jJ5nYdE1aHxAGI0_qbV91-FhUo3uwPulpkEvZVMoo",{"id":630,"title":631,"body":632,"date":896,"description":897,"draft":182,"extension":183,"meta":898,"navigation":185,"path":899,"seo":900,"stem":901,"tags":902,"__hash__":907},"blog\u002Fblog\u002Farchitecture-decision-records.md","Architecture Decision Records: The Best Habit My Team Adopted",{"type":8,"value":633,"toc":890},[634,637,640,643,649,670,674,677,697,711,715,718,837,844,848,851,877,881,884,887],[11,635,636],{},"Every codebase has decisions baked into it that nobody remembers making.",[11,638,639],{},"Why is this service synchronous instead of async? Why does this module use Repository pattern but that one doesn't? Why are we on PHP 8.3 instead of 8.4?",[11,641,642],{},"The answer is usually \"because someone decided that two years ago and we don't know why.\"",[11,644,645,648],{},[31,646,647],{},"Architecture Decision Records"," (ADRs) are the fix. They're short documents — often just a page — that record:",[60,650,651,658,664],{},[63,652,653,654,657],{},"The ",[31,655,656],{},"context"," (what problem were we solving?)",[63,659,653,660,663],{},[31,661,662],{},"decision"," (what did we choose?)",[63,665,653,666,669],{},[31,667,668],{},"consequences"," (what does this mean going forward?)",[23,671,673],{"id":672},"what-we-actually-did","What we actually did",[11,675,676],{},"When I joined the team, the codebase was brownfield — years of accumulated decisions with no written rationale. That made it the perfect moment to change the habit. I introduced ADRs and wrote the first 11 myself, covering:",[60,678,679,682,685,688,691,694],{},[63,680,681],{},"Module boundary rules (enforced by Deptrac)",[63,683,684],{},"Database interaction patterns (no raw SQL outside Repository classes)",[63,686,687],{},"Naming conventions for handlers, DTOs, and services",[63,689,690],{},"When to use Doctrine vs raw PDO",[63,692,693],{},"How to version the OpenAPI spec",[63,695,696],{},"CI quality gate requirements",[11,698,699,700,703,704,516,707,710],{},"They live in ",[18,701,702],{},"docs\u002Fadr\u002F"," in the repo, numbered sequentially: ",[18,705,706],{},"0001-module-boundaries.md",[18,708,709],{},"0002-database-patterns.md",", etc.",[23,712,714],{"id":713},"the-format-that-stuck","The format that stuck",[11,716,717],{},"I tried multiple ADR formats. The one that the team actually used was the shortest one:",[222,719,723],{"className":720,"code":721,"language":722,"meta":172,"style":172},"language-markdown shiki shiki-themes one-dark-pro","# ADR 0001: Module boundaries enforced by Deptrac\n\n## Status\n\nAccepted\n\n## Context\n\nWithout explicit boundaries, modules will import from each other freely,\nmaking the codebase increasingly tangled over time.\n\n## Decision\n\nEach module may only depend on its direct dependencies as defined in\ndeptrac.yaml. Cross-module access goes through interfaces.\n\n## Consequences\n\n- New features must think about which module they belong to\n- Deptrac runs in CI; violations fail the build\n- Refactoring across modules requires updating deptrac.yaml explicitly\n","markdown",[18,724,725,730,734,739,743,748,752,757,761,766,771,775,780,784,790,796,801,807,812,821,829],{"__ignoreMap":172},[230,726,727],{"class":232,"line":233},[230,728,729],{"class":489},"# ADR 0001: Module boundaries enforced by Deptrac\n",[230,731,732],{"class":232,"line":173},[230,733,250],{"emptyLinePlaceholder":185},[230,735,736],{"class":232,"line":253},[230,737,738],{"class":489},"## Status\n",[230,740,741],{"class":232,"line":259},[230,742,250],{"emptyLinePlaceholder":185},[230,744,745],{"class":232,"line":272},[230,746,747],{"class":244},"Accepted\n",[230,749,750],{"class":232,"line":283},[230,751,250],{"emptyLinePlaceholder":185},[230,753,754],{"class":232,"line":289},[230,755,756],{"class":489},"## Context\n",[230,758,759],{"class":232,"line":346},[230,760,250],{"emptyLinePlaceholder":185},[230,762,763],{"class":232,"line":352},[230,764,765],{"class":244},"Without explicit boundaries, modules will import from each other freely,\n",[230,767,768],{"class":232,"line":358},[230,769,770],{"class":244},"making the codebase increasingly tangled over time.\n",[230,772,773],{"class":232,"line":366},[230,774,250],{"emptyLinePlaceholder":185},[230,776,777],{"class":232,"line":372},[230,778,779],{"class":489},"## Decision\n",[230,781,782],{"class":232,"line":379},[230,783,250],{"emptyLinePlaceholder":185},[230,785,787],{"class":232,"line":786},14,[230,788,789],{"class":244},"Each module may only depend on its direct dependencies as defined in\n",[230,791,793],{"class":232,"line":792},15,[230,794,795],{"class":244},"deptrac.yaml. Cross-module access goes through interfaces.\n",[230,797,799],{"class":232,"line":798},16,[230,800,250],{"emptyLinePlaceholder":185},[230,802,804],{"class":232,"line":803},17,[230,805,806],{"class":489},"## Consequences\n",[230,808,810],{"class":232,"line":809},18,[230,811,250],{"emptyLinePlaceholder":185},[230,813,815,818],{"class":232,"line":814},19,[230,816,817],{"class":298},"-",[230,819,820],{"class":244}," New features must think about which module they belong to\n",[230,822,824,826],{"class":232,"line":823},20,[230,825,817],{"class":298},[230,827,828],{"class":244}," Deptrac runs in CI; violations fail the build\n",[230,830,832,834],{"class":232,"line":831},21,[230,833,817],{"class":298},[230,835,836],{"class":244}," Refactoring across modules requires updating deptrac.yaml explicitly\n",[11,838,839,840,843],{},"That's it. The key is: write it ",[167,841,842],{},"before"," you implement, while the reasoning is fresh.",[23,845,847],{"id":846},"getting-the-team-to-write-them","Getting the team to write them",[11,849,850],{},"The hardest part is habit formation. What worked for us:",[95,852,853,859,865,871],{},[63,854,855,858],{},[31,856,857],{},"Led by example"," — I wrote the first batch before asking anyone else to",[63,860,861,864],{},[31,862,863],{},"PR requirement"," — significant architectural changes needed a linked ADR",[63,866,867,870],{},[31,868,869],{},"No perfectionism"," — a rough ADR written in 20 minutes beats a perfect one that never gets written",[63,872,873,876],{},[31,874,875],{},"Reference them in code review"," — \"this violates ADR 0003, here's the link\"",[23,878,880],{"id":879},"the-compounding-return","The compounding return",[11,882,883],{},"Six months in, the value becomes obvious. When a new developer joins and asks \"why do we do X this way?\" — you have an answer. When you're about to make a decision that conflicts with a past one, you catch it. When you're debugging something weird, the decision log is a map.",[11,885,886],{},"The investment is small. The return is compounding. Start your ADR log today.",[608,888,889],{},"html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":172,"searchDepth":173,"depth":173,"links":891},[892,893,894,895],{"id":672,"depth":173,"text":673},{"id":713,"depth":173,"text":714},{"id":846,"depth":173,"text":847},{"id":879,"depth":173,"text":880},"2026-01-20","Why writing down the 'why' behind technical decisions — even briefly — pays compounding returns over time, and how to get a team to actually do it.",{},"\u002Fblog\u002Farchitecture-decision-records",{"title":631,"description":897},"blog\u002Farchitecture-decision-records",[903,904,905,906],"Architecture","Engineering Culture","Documentation","ADR","3zBOlesZjrM9yrzPwrRgy77peiULqPIiGOZhkAeOgBM",1776339762988]