[{"data":1,"prerenderedAt":1307},["ShallowReactive",2],{"navigation":3,"blog-page":34,"blogs":44},[4],{"title":5,"path":6,"stem":7,"children":8,"page":33},"Blog","\u002Fblog","blog",[9,13,17,21,25,29],{"title":10,"path":11,"stem":12},"Building a Reusable Skill for Vue 3: From Architecture Principles to a Published GitHub Standard","\u002Fblog\u002Fbuilding-a-reusable-skill-for-vue-3","blog\u002Fbuilding-a-reusable-skill-for-vue-3",{"title":14,"path":15,"stem":16},"From Engineer to Lead: The Shift Nobody Prepares You For","\u002Fblog\u002Ffrom-engineer-to-lead","blog\u002Ffrom-engineer-to-lead",{"title":18,"path":19,"stem":20},"Paginating Complex CTEs in PostgreSQL: Lessons from a Real Chat App","\u002Fblog\u002Fpaginating-complex-ctes-in-postgresql","blog\u002Fpaginating-complex-ctes-in-postgresql",{"title":22,"path":23,"stem":24},"Scaling Frontend Teams Without Losing Code Quality","\u002Fblog\u002Fscaling-frontend-teams-without-losing-code-quality","blog\u002Fscaling-frontend-teams-without-losing-code-quality",{"title":26,"path":27,"stem":28},"Taming Supabase Realtime in a Tauri App: Sleep, Wake, and Reconnection","\u002Fblog\u002Ftaming-supabase-realtime-in-a-tauri-app","blog\u002Ftaming-supabase-realtime-in-a-tauri-app",{"title":30,"path":31,"stem":32},"Why I Stopped Writing Components and Started Writing Composables","\u002Fblog\u002Fwhy-i-stopped-writing-components-and-started-writing-composables","blog\u002Fwhy-i-stopped-writing-components-and-started-writing-composables",false,{"id":35,"title":36,"body":37,"description":38,"extension":39,"links":37,"meta":40,"navigation":41,"path":6,"seo":42,"stem":7,"__hash__":43},"pages\u002Fblog.yml","Latest Articles",null,"Some of my recent thoughts on design, development, and the tech industry.","yml",{},true,{"title":36,"description":38},"vycKbZXH6lUprkttlxVokzcLu31TOtP82HO4ECYI1yY",[45,215,351,545,741,947],{"id":46,"title":30,"author":47,"body":53,"date":207,"description":208,"extension":209,"image":210,"meta":211,"minRead":212,"navigation":41,"path":31,"seo":213,"stem":32,"__hash__":214},"blog\u002Fblog\u002Fwhy-i-stopped-writing-components-and-started-writing-composables.md",{"name":48,"username":49,"to":50,"avatar":51},"Junaid Rasheed","junaidrasheed","https:\u002F\u002Fgithub.com\u002Fjunaidrasheed",{"src":52,"alt":48},"\u002Favatars\u002Fme.png",{"type":54,"value":55,"toc":197},"minimark",[56,60,75,80,83,90,96,100,103,106,109,113,119,125,131,135,138,156,162,168,174,178,181,184,187,191,194],[57,58,59],"p",{},"There's a pattern every Vue developer goes through. You start writing components. Then you write more components. Then you write components that contain other components. Then one day you open a file and there's a 600-line component that does seventeen things and you have no idea how it got that way.",[57,61,62,63,67,68,67,71,74],{},"The answer is always the same: you put logic where the framework made it easy to put logic. And in Vue 2, that was the component. The Options API encouraged it. ",[64,65,66],"code",{},"data",", ",[64,69,70],{},"methods",[64,72,73],{},"computed"," — all neatly organized by type, not by concern. It looked tidy. It wasn't.",[76,77,79],"h2",{"id":78},"the-real-problem-with-component-first-thinking","The real problem with component-first thinking",[57,81,82],{},"The issue isn't components themselves — it's using them as the primary unit of reuse. When your instinct is \"I need to reuse this, I'll make a component,\" you end up with two problems.",[57,84,85,89],{},[86,87,88],"strong",{},"You couple logic to rendering."," A component that fetches data and displays it is doing two completely separate jobs. Reusing the display means dragging along the fetching. Reusing the fetching means dragging along the display. Neither is what you actually wanted.",[57,91,92,95],{},[86,93,94],{},"You create implicit dependencies."," Components communicate through props and emits, which is fine at small scale and progressively painful as the tree deepens. By the time you're prop-drilling through four levels or reaching for an event bus, you've built a distributed system inside your UI layer. Congratulations, you've invented microservices for your frontend. Nobody is happy about this.",[76,97,99],{"id":98},"what-composables-actually-are","What composables actually are",[57,101,102],{},"The Vue 3 Composition API introduced composables — functions that encapsulate reactive logic and can be used in any component, regardless of what that component renders.",[57,104,105],{},"The mental model shift is this: a composable is not a component without a template. It's a unit of behavior. It owns state and the logic that operates on that state. It doesn't care about rendering at all.",[57,107,108],{},"A component's job is to decide what to show and how to respond to user input. A composable's job is to manage the logic and state that the component needs to do that. When you split them cleanly, both become dramatically simpler.",[76,110,112],{"id":111},"where-the-shift-shows-up","Where the shift shows up",[57,114,115,118],{},[86,116,117],{},"Data fetching."," This is the clearest win. A composable that handles fetching, loading state, error state, and retry logic can be dropped into any component that needs it. The component just consumes the result. You write the fetching logic once, test it independently, and never think about it again in the context of a specific component.",[57,120,121,124],{},[86,122,123],{},"Form handling."," Forms are where component-first thinking produces the most pain. Validation logic, field state, submission handling, error display — none of that needs to live in the component. A composable that takes a schema and returns field state and validation results keeps the component focused on layout and nothing else.",[57,126,127,130],{},[86,128,129],{},"Shared reactive state."," When multiple components need access to the same piece of state, the composable becomes the single source of truth. Not a store for everything — just a scoped, intentional owner of one concept. This is often the right tool before reaching for a full state management solution.",[76,132,134],{"id":133},"the-rules-that-make-composables-actually-work","The rules that make composables actually work",[57,136,137],{},"Not all composables are created equal. The pattern can produce beautiful, reusable logic or a different kind of mess if you're not deliberate about it.",[57,139,140,143,144,147,148,151,152,155],{},[86,141,142],{},"Name them by what they do, not what they're for."," ",[64,145,146],{},"useUserProfile"," is a component-specific name. ",[64,149,150],{},"useAsyncData"," or ",[64,153,154],{},"useFormValidation"," is a behavioral name. The behavioral name travels. The component-specific name doesn't.",[57,157,158,161],{},[86,159,160],{},"Return only what the consumer needs."," It's tempting to expose everything from a composable \"just in case.\" Resist this. Every exposed value is a contract. The smaller the surface area, the easier the composable is to use, test, and change later.",[57,163,164,167],{},[86,165,166],{},"Keep them focused on one concern."," A composable that handles fetching AND caching AND error formatting AND retry logic is a component in disguise. Split it. Composables compose — that's the whole point. A composable can call another composable.",[57,169,170,173],{},[86,171,172],{},"Don't reach for composables for everything."," Simple derived state that's only used in one place belongs in the component. The overhead of extracting it into a composable isn't worth it. Extract when there's a genuine reuse case or when the logic is complex enough to warrant isolated testing.",[76,175,177],{"id":176},"the-testing-benefit-nobody-talks-about-enough","The testing benefit nobody talks about enough",[57,179,180],{},"Composables are just functions. Functions are easy to test. You don't need to mount a component, simulate user interactions, or worry about the DOM. You call the function, interact with the returned state, and assert the result.",[57,182,183],{},"This is a significant quality-of-life improvement. Testing component logic that lives inside the component requires testing through the component. Testing the same logic in a composable is three lines of setup. The tests are faster, more focused, and much easier to read and maintain.",[57,185,186],{},"If you find yourself writing complex component tests to cover logic, that logic probably belongs in a composable.",[76,188,190],{"id":189},"the-mental-model-in-one-sentence","The mental model in one sentence",[57,192,193],{},"Components decide what to show. Composables decide what's true.",[57,195,196],{},"When that division is clear, components get simpler, logic gets reusable, and the codebase becomes something you can actually navigate six months after writing it. Which, in frontend development, is about as close to utopia as we get.",{"title":198,"searchDepth":199,"depth":199,"links":200},"",2,[201,202,203,204,205,206],{"id":78,"depth":199,"text":79},{"id":98,"depth":199,"text":99},{"id":111,"depth":199,"text":112},{"id":133,"depth":199,"text":134},{"id":176,"depth":199,"text":177},{"id":189,"depth":199,"text":190},"2026-06-17","Using components as the primary unit of reuse couples logic to rendering and breeds 600-line files. The mental model shift that makes Vue 3 codebases navigable six months later.","md","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F3993855\u002Fpexels-photo-3993855.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},6,{"title":30,"description":208},"rsmg2K7YAeFKmI0g4TT2ZqBDMGCq1aFiwcK0LUquPNU",{"id":216,"title":14,"author":217,"body":219,"date":344,"description":345,"extension":209,"image":346,"meta":347,"minRead":348,"navigation":41,"path":15,"seo":349,"stem":16,"__hash__":350},"blog\u002Fblog\u002Ffrom-engineer-to-lead.md",{"name":48,"username":49,"to":50,"avatar":218},{"src":52,"alt":48},{"type":54,"value":220,"toc":336},[221,224,227,231,234,237,240,244,247,250,253,256,260,266,272,278,284,288,291,304,307,310,314,317,320,323,327,330,333],[57,222,223],{},"Nobody gives you a manual when you become a lead. One day you're heads-down in a ticket, the next you're running a planning meeting, unblocking three people simultaneously, and somehow still expected to ship code. It's a role that sounds like a promotion and feels like a completely different job.",[57,225,226],{},"Because it is.",[76,228,230],{"id":229},"the-thing-they-dont-tell-you","The thing they don't tell you",[57,232,233],{},"When you're an individual contributor, your output is visible and measurable. You shipped the feature. You fixed the bug. You can point to the PR. At the end of the day, you know whether you did your job.",[57,235,236],{},"When you become a lead, your best work becomes invisible. You asked the right question in a planning meeting that prevented two weeks of rework. You gave feedback on a junior engineer's approach before they went down the wrong path. You noticed that two teams were solving the same problem differently and quietly aligned them. None of that shows up in a commit history.",[57,238,239],{},"This is disorienting at first. Engineers are trained to measure their value by what they build. Leads create value by shaping what others build — and that requires a completely different feedback loop.",[76,241,243],{"id":242},"the-coding-trap","The coding trap",[57,245,246],{},"Most new leads fall into one of two failure modes.",[57,248,249],{},"The first is holding on too tightly to the code. You're still the best technical contributor on the team, and it feels good to be in the code. So you stay there. You take the hardest tickets. You become the bottleneck for every complex problem. The team grows but your habits don't, and six months later you're overwhelmed, your engineers aren't growing, and nothing ships without you.",[57,251,252],{},"The second is abandoning the code entirely. You've heard that \"real leads don't code\" (you haven't, but it feels implied), so you step back. You go full calendar. You lose touch with the codebase, your technical credibility starts to erode, and you find yourself unable to push back on estimates or spot bad architectural decisions because you're no longer close enough to the work.",[57,254,255],{},"The actual job lives in the tension between these two. You need to stay technical enough to be credible and useful, while deliberately creating space for your engineers to own the hard problems. That balance shifts constantly and there's no formula for it.",[76,257,259],{"id":258},"what-actually-changes","What actually changes",[57,261,262,265],{},[86,263,264],{},"Your definition of \"done\" expands."," A feature isn't done when the code is merged. It's done when the engineer who built it understands it well enough to maintain it, when the edge cases are covered, when the next engineer can pick it up without a walkthrough. You start thinking in systems rather than tasks.",[57,267,268,271],{},[86,269,270],{},"Ambiguity becomes your daily environment."," Individual contributors mostly work with well-defined problems. Leads work with poorly defined ones. \"We need to improve performance\" is not a ticket — it's a direction. Translating vague organizational intent into concrete, actionable work for your team is a skill you have to develop from scratch, and it's harder than any technical problem you've solved.",[57,273,274,277],{},[86,275,276],{},"Your calendar becomes the product."," How you spend your time directly determines what your team can and can't do. A blocked engineer who can't get 30 minutes with you this week is an engineer who loses a day or more of progress. Time management stops being a personal productivity habit and becomes a team responsibility.",[57,279,280,283],{},[86,281,282],{},"You absorb uncertainty so your team doesn't have to."," Priorities shift. Roadmaps change. Stakeholders disagree. A big part of the lead role is acting as a buffer — taking in the chaos from above and translating it into something stable and actionable for the engineers on your team. They should be able to focus. That focus is something you actively protect.",[76,285,287],{"id":286},"the-mentoring-misconception","The mentoring misconception",[57,289,290],{},"A lot of new leads think mentoring means teaching. It doesn't — or at least, not primarily. The most effective mentoring is mostly asking questions.",[292,293,294,298,301],"ul",{},[295,296,297],"li",{},"\"What approaches did you consider?\"",[295,299,300],{},"\"What do you think the tradeoff is here?\"",[295,302,303],{},"\"How would you handle it if this constraint changed?\"",[57,305,306],{},"When you give an engineer the answer, they learn the answer. When you ask them the right questions, they learn how to think. The second one scales. The first one just makes them dependent on you for the next problem.",[57,308,309],{},"This is also better for you. If your engineers can reason through hard problems independently, you're not the ceiling on what they can accomplish. That's how you build a team that can operate without you in the room — which is, counterintuitively, exactly what a good lead aims for.",[76,311,313],{"id":312},"the-visibility-problem","The visibility problem",[57,315,316],{},"Here's something that takes too long to learn: if your work is invisible, you have to make it visible.",[57,318,319],{},"Not in a self-promotional way — in a communication way. Write up the architectural decision you steered. Share the context behind a tradeoff in your team channel. Send the summary after a difficult planning session. These aren't vanity exercises. They create a paper trail of the thinking that's actually driving the team, they help engineers understand context they wouldn't otherwise have, and they make your contributions legible to the people who make decisions about your career.",[57,321,322],{},"Individual contributors can sometimes get away with letting the code speak for itself. Leads can't. The work is too distributed, too contextual, and too process-oriented to be self-evident. You have to narrate it.",[76,324,326],{"id":325},"the-honest-summary","The honest summary",[57,328,329],{},"Becoming a lead is not a reward for being a great engineer. It's a different role that requires different skills, a different feedback loop, and a willingness to measure your success by other people's growth rather than your own output.",[57,331,332],{},"Some great engineers make terrible leads. Some average engineers make exceptional leads. The skills barely overlap.",[57,334,335],{},"If you're considering the transition, go in with eyes open. The job is harder in ways you won't anticipate, more rewarding in ways you won't expect, and almost nothing like what you imagined. That's not a warning — it's just the truth. And knowing it going in puts you ahead of where most people start.",{"title":198,"searchDepth":199,"depth":199,"links":337},[338,339,340,341,342,343],{"id":229,"depth":199,"text":230},{"id":242,"depth":199,"text":243},{"id":258,"depth":199,"text":259},{"id":286,"depth":199,"text":287},{"id":312,"depth":199,"text":313},{"id":325,"depth":199,"text":326},"2026-06-10","Becoming a lead sounds like a promotion and feels like a completely different job — because it is. On invisible work, the coding trap, and measuring success by other people's growth.","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F3862615\u002Fpexels-photo-3862615.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},7,{"title":14,"description":345},"t5M1mNwfk-dxDnwLlPx2OO8J2saOrV5N7tOFEfxQ9Uo",{"id":352,"title":10,"author":353,"body":355,"date":539,"description":540,"extension":209,"image":541,"meta":542,"minRead":348,"navigation":41,"path":11,"seo":543,"stem":12,"__hash__":544},"blog\u002Fblog\u002Fbuilding-a-reusable-skill-for-vue-3.md",{"name":48,"username":49,"to":50,"avatar":354},{"src":52,"alt":48},{"type":54,"value":356,"toc":531},[357,360,363,366,370,373,376,379,383,395,398,401,405,408,428,431,434,460,464,475,478,481,484,488,491,494,500,506,512,516,519,522,525,528],[57,358,359],{},"At some point in every senior engineer's career, you have a realization: you've explained the same architectural pattern for the seventh time this month. Different engineer, different PR, same feedback. Same problem, same solution, same comment written slightly differently.",[57,361,362],{},"That's not mentoring. That's a missing abstraction.",[57,364,365],{},"This post is about what I did about it — and why codifying your standards into something shareable is one of the highest-leverage things a senior engineer can do.",[76,367,369],{"id":368},"the-problem-with-tribal-knowledge","The problem with tribal knowledge",[57,371,372],{},"Most teams run on tribal knowledge. The senior engineers know how things should be done. Junior engineers learn by getting feedback on PRs, asking questions in Slack, and gradually absorbing the patterns over months of exposure. It works, eventually. But it's slow, it's inconsistent, and it scales terribly.",[57,374,375],{},"The senior engineer becomes a bottleneck. Every non-trivial PR needs their eye on it. Onboarding new engineers takes forever because there's no single source of truth — just a collection of examples scattered across the codebase and a handful of people who hold the context in their heads.",[57,377,378],{},"The worst part? When those people leave, the knowledge leaves with them. Tribal knowledge has no version history.",[76,380,382],{"id":381},"what-a-skill-actually-is","What a \"skill\" actually is",[57,384,385,386,390,391,394],{},"I started thinking about this differently: what if the patterns I kept explaining in reviews were just... documentation? Not a wiki page that nobody reads, but an opinionated, structured reference that captures not just ",[387,388,389],"em",{},"what"," to do but ",[387,392,393],{},"why"," — and that could be handed to any engineer (or AI coding assistant) and immediately produce code at the standard you actually want.",[57,396,397],{},"That's what I built for Vue 3. A skill — a compact, structured document that encodes senior-level patterns for the Composition API: how composables should be structured, how state should be managed, how components should be split, how validation should be handled, how tests should be written.",[57,399,400],{},"Not a style guide. Not a list of rules. A working standard with rationale.",[76,402,404],{"id":403},"what-goes-into-it","What goes into it",[57,406,407],{},"The key insight is that a useful standard has to answer three questions for every pattern it covers:",[292,409,410,416,422],{},[295,411,412,415],{},[86,413,414],{},"What"," — the concrete pattern or structure being recommended",[295,417,418,421],{},[86,419,420],{},"Why"," — the reasoning behind it, not just \"because I said so\"",[295,423,424,427],{},[86,425,426],{},"What not to do"," — the anti-pattern it replaces, and why that's worse",[57,429,430],{},"Most style guides only answer the first question. That's why engineers ignore them under pressure — they follow rules they understand and skip the ones that feel arbitrary. When the reasoning is visible, engineers internalize the principle, not just the syntax. They apply it in situations the guide never anticipated.",[57,432,433],{},"For Vue 3 specifically, the areas that needed the most opinionated guidance were:",[292,435,436,442,448,454],{},[295,437,438,441],{},[86,439,440],{},"Composable design"," — when to extract, how to name, what to expose",[295,443,444,447],{},[86,445,446],{},"Component responsibility"," — the boundary between \"smart\" and \"dumb\" components",[295,449,450,453],{},[86,451,452],{},"TypeScript integration"," — not just \"use types\" but how to type props, emits, and composable return values in a way that's actually useful",[295,455,456,459],{},[86,457,458],{},"Testing philosophy"," — what to test, what not to test, and why unit testing implementation details is a trap",[76,461,463],{"id":462},"the-process-of-writing-it","The process of writing it",[57,465,466,467,470,471,474],{},"Writing this kind of document is harder than it sounds, for one reason: you have to separate what you ",[387,468,469],{},"do"," from what you ",[387,472,473],{},"should do",".",[57,476,477],{},"Senior engineers accumulate habits. Some of those habits are genuinely good patterns. Some are historical artifacts from older versions of the framework, or workarounds for problems that no longer exist, or just personal preferences that never got challenged. Writing a standard forces you to interrogate your own practices.",[57,479,480],{},"\"Why do I do it this way? Is this actually better, or is it just familiar?\"",[57,482,483],{},"That process is uncomfortable and extremely valuable. I rewrote sections multiple times after realizing I was documenting a habit rather than a principle. The final document was better for it — and so was my own understanding of the codebase.",[76,485,487],{"id":486},"communicating-it-to-the-team","Communicating it to the team",[57,489,490],{},"A standard nobody knows about is just a file on GitHub. Getting the team to actually use it requires a different kind of effort than writing it.",[57,492,493],{},"A few things that helped:",[57,495,496,499],{},[86,497,498],{},"Frame it as a resource, not a mandate."," Engineers who feel like standards are being imposed on them resist them. Engineers who feel like they have access to a useful reference use it. The framing matters more than you'd expect.",[57,501,502,505],{},[86,503,504],{},"Walk through it in a team session."," Not a lecture — a conversation. Let engineers push back on the patterns, ask why, suggest alternatives. Some of the best additions to the standard came from those discussions. It also creates shared ownership, which is what makes standards actually stick.",[57,507,508,511],{},[86,509,510],{},"Reference it in reviews, don't replace reviews with it."," When you leave a comment pointing to a section of the standard, you're doing two things: giving specific actionable feedback, and reinforcing that the standard exists and is being actively used. Eventually engineers start referencing it themselves before submitting PRs.",[76,513,515],{"id":514},"why-this-is-worth-your-time","Why this is worth your time",[57,517,518],{},"The ROI on this kind of work is slow and invisible at first. You write the standard, you share it, and nothing obviously changes the next day.",[57,520,521],{},"But three months later, the PR feedback you're leaving has shifted. You're catching architectural issues earlier, because engineers are catching the obvious ones themselves. Onboarding new engineers is faster because there's something concrete to point them to. The tribal knowledge has a home. It's versioned. It can be updated. It can be shared outside the team.",[57,523,524],{},"And the next time you find yourself writing the same code review comment for the eighth time, you don't write it — you update the standard instead.",[57,526,527],{},"That's the compounding return. One document, maintained well, that makes every future review, every onboarding, and every architectural discussion slightly more efficient. Over a year, that's an enormous amount of recovered time and transferred knowledge.",[57,529,530],{},"Senior engineers are often evaluated on their code. The best ones get evaluated on how much better the engineers around them become. A published standard is one of the clearest ways to make that impact visible.",{"title":198,"searchDepth":199,"depth":199,"links":532},[533,534,535,536,537,538],{"id":368,"depth":199,"text":369},{"id":381,"depth":199,"text":382},{"id":403,"depth":199,"text":404},{"id":462,"depth":199,"text":463},{"id":486,"depth":199,"text":487},{"id":514,"depth":199,"text":515},"2026-06-03","When you've explained the same architectural pattern for the seventh time this month, that's not mentoring — it's a missing abstraction. How I codified senior-level Vue 3 patterns into a shareable standard.","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F2004161\u002Fpexels-photo-2004161.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},{"title":10,"description":540},"mwCKOocAKp9BzT2E8z1dy1gWenUdE_oV0hlZjOc4vjI",{"id":546,"title":22,"author":547,"body":549,"date":734,"description":735,"extension":209,"image":736,"meta":737,"minRead":738,"navigation":41,"path":23,"seo":739,"stem":24,"__hash__":740},"blog\u002Fblog\u002Fscaling-frontend-teams-without-losing-code-quality.md",{"name":48,"username":49,"to":50,"avatar":548},{"src":52,"alt":48},{"type":54,"value":550,"toc":725},[551,554,557,561,564,567,570,574,577,587,593,599,603,606,615,621,627,637,641,644,647,653,656,660,663,666,669,673,676,709,712,716,719,722],[57,552,553],{},"There's a version of this post that's just \"write tests, do code reviews, use a linter.\" You've read that post. Everyone has read that post. This isn't that post.",[57,555,556],{},"This is about what actually breaks when your frontend team grows from 5 to 15 to 30 engineers — and what enforcement mechanisms survive contact with real humans under real deadlines.",[76,558,560],{"id":559},"the-illusion-of-shared-standards","The illusion of shared standards",[57,562,563],{},"Early-stage teams have high code consistency almost by accident. There are three of you, you sit near each other (physically or on Slack), and you naturally converge on patterns just by reading each other's PRs. The codebase is coherent because the team is small enough to have a shared mental model without ever writing it down.",[57,565,566],{},"Then you hire. And hire again. And suddenly there are six different ways to handle form validation across the codebase, nobody agrees on where state should live, and every PR introduces a pattern the reviewer has never seen before. The code still works. It just looks like it was written by people who've never met — because increasingly, it was.",[57,568,569],{},"This is not a people problem. It's a systems problem. And systems problems need systems solutions.",[76,571,573],{"id":572},"what-actually-enforces-standards-and-what-doesnt","What actually enforces standards (and what doesn't)",[57,575,576],{},"Let's be honest about the tools.",[57,578,579,582,583,586],{},[86,580,581],{},"Documentation doesn't scale."," A Confluence page or a ",[64,584,585],{},"CONTRIBUTING.md"," that nobody reads isn't a standard — it's a wishlist. Documentation is great for context and rationale, but it can't enforce anything. It relies entirely on engineers choosing to look it up, which they won't do under deadline pressure.",[57,588,589,592],{},[86,590,591],{},"Code reviews help but aren't enough."," Reviews catch issues after the fact. By the time a reviewer is pointing out that strict TypeScript types are being bypassed, the engineer has already committed mentally to their approach. You're adding friction late in the process rather than preventing the problem early.",[57,594,595,598],{},[86,596,597],{},"Automation is the only thing that scales."," Rules that are enforced by tooling are enforced consistently, at the right time, by every engineer, regardless of experience level or deadline pressure. This is not about distrust — it's about removing the cognitive overhead of remembering every standard from every person on every PR.",[76,600,602],{"id":601},"the-enforcement-stack-that-holds-up","The enforcement stack that holds up",[57,604,605],{},"After leading teams across multiple fast-growth environments, here's the combination that actually works.",[57,607,608,143,611,614],{},[86,609,610],{},"TypeScript strict mode, non-negotiable.",[64,612,613],{},"strict: true"," in your tsconfig is the single highest-leverage configuration change you can make. It forces explicit types, catches null-related bugs at compile time, and makes the codebase dramatically easier to refactor as it grows. The initial pain of enabling it is real. The long-term payoff is realer. Treat any PR that disables strict checks as a red flag, not a reasonable shortcut.",[57,616,617,620],{},[86,618,619],{},"ESLint with team-agreed rules, checked in and documented."," The rules themselves matter less than the fact that they're agreed upon and enforced consistently. When a lint rule exists, it's never a personal critique — it's just the rule. This removes a surprising amount of ego from code review conversations. \"The linter flagged it\" is a much easier sentence than \"I don't think you should do it that way.\"",[57,622,623,626],{},[86,624,625],{},"Pre-commit hooks for the basics."," Linting and type checking on commit means problems surface before they ever touch a PR. Engineers get immediate, local feedback rather than finding out in review that their branch has 40 lint errors to clean up. Tools like Husky make this straightforward to set up and hard to accidentally skip.",[57,628,629,632,633,636],{},[86,630,631],{},"Automated checks in CI as the final gate."," Pre-commit hooks can be bypassed (",[64,634,635],{},"--no-verify"," exists, and engineers will use it when they're in a hurry). CI can't be bypassed. If type checks, lint, and tests must pass before a PR can be merged, the gate is real regardless of what happened locally. No exceptions, no \"I'll fix it in a follow-up.\"",[76,638,640],{"id":639},"the-human-side-of-enforcement","The human side of enforcement",[57,642,643],{},"Here's the part tooling can't do: getting engineers to buy into the standards in the first place.",[57,645,646],{},"Mandated rules that engineers don't understand or disagree with create resentment. They find workarounds. They disable checks. They treat the tooling as an obstacle rather than a collaborator. You end up in a constant arms race between enforcement and evasion, which is exhausting for everyone.",[57,648,649,650,652],{},"The better approach is to involve the team in setting the standards. When engineers participate in the decision to enable strict TypeScript or adopt a particular ESLint ruleset, they understand ",[387,651,393],{}," the rule exists. That understanding travels with them into code reviews, architecture discussions, and eventually into how they mentor the engineers who join after them.",[57,654,655],{},"Standards that the team owns propagate themselves. Standards that are handed down from above get worked around.",[76,657,659],{"id":658},"pr-culture-is-the-multiplier","PR culture is the multiplier",[57,661,662],{},"All of the above is table stakes. The real multiplier is a healthy PR culture where reviews are fast, feedback is specific, and context is always provided.",[57,664,665],{},"Slow reviews kill momentum and teach engineers to batch up large PRs to avoid the wait — which makes reviews worse, which makes them slower. It's a doom loop. Keeping PRs small and reviews quick is a discipline that requires active maintenance from leads, not just a guideline in a doc.",[57,667,668],{},"Specific feedback matters too. \"This could be cleaner\" is not useful. \"This component is doing three things — fetching data, transforming it, and rendering — consider splitting the fetch into a composable so the component only handles display\" is useful. The more specific your feedback, the faster junior engineers grow, and the less you need to repeat yourself across reviews.",[76,670,672],{"id":671},"what-breaks-first-at-scale","What breaks first at scale",[57,674,675],{},"In order of how quickly they degrade as teams grow:",[677,678,679,685,691,697,703],"ol",{},[295,680,681,684],{},[86,682,683],{},"Naming conventions"," — fast to diverge, slow to reconcile",[295,686,687,690],{},[86,688,689],{},"State management patterns"," — everyone has opinions, nobody agrees",[295,692,693,696],{},[86,694,695],{},"Error handling"," — inconsistent until a production incident forces a standard",[295,698,699,702],{},[86,700,701],{},"Test coverage"," — the first thing cut under deadline pressure, every time",[295,704,705,708],{},[86,706,707],{},"Component boundaries"," — gradually becomes \"this component does everything\"",[57,710,711],{},"Knowing what breaks first tells you where to invest in tooling and documentation early, before the team is large enough that retrofitting becomes painful.",[76,713,715],{"id":714},"the-goal-isnt-uniformity-its-predictability","The goal isn't uniformity — it's predictability",[57,717,718],{},"The best codebases aren't ones where every component looks identical. They're ones where any engineer can open any file and immediately understand what's happening, make a change with confidence, and not accidentally break something three modules away.",[57,720,721],{},"That predictability is what scales. It's what makes onboarding faster, debugging faster, and refactoring possible without a full team war room. And it doesn't come from hiring great engineers — it comes from building systems that help good engineers do their best work consistently.",[57,723,724],{},"Turns out, that's most of the job.",{"title":198,"searchDepth":199,"depth":199,"links":726},[727,728,729,730,731,732,733],{"id":559,"depth":199,"text":560},{"id":572,"depth":199,"text":573},{"id":601,"depth":199,"text":602},{"id":639,"depth":199,"text":640},{"id":658,"depth":199,"text":659},{"id":671,"depth":199,"text":672},{"id":714,"depth":199,"text":715},"2026-05-27","What actually breaks when a frontend team grows from 5 to 30 engineers, and which enforcement mechanisms survive contact with real humans under real deadlines.","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F7212946\u002Fpexels-photo-7212946.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},8,{"title":22,"description":735},"NVacF9IvrND8PCcs-nKxQmrUUHXTnZs-SaD1T63_LVc",{"id":742,"title":18,"author":743,"body":745,"date":941,"description":942,"extension":209,"image":943,"meta":944,"minRead":348,"navigation":41,"path":19,"seo":945,"stem":20,"__hash__":946},"blog\u002Fblog\u002Fpaginating-complex-ctes-in-postgresql.md",{"name":48,"username":49,"to":50,"avatar":744},{"src":52,"alt":48},{"type":54,"value":746,"toc":932},[747,750,753,757,760,763,766,770,773,776,779,794,797,801,804,807,810,834,837,841,844,851,854,858,865,878,884,888,891,894,897,901,925,928],[57,748,749],{},"Pagination. The feature that sounds like it takes an afternoon and somehow consumes three days of your life. You've been there. We've all been there. Now imagine doing it inside a CTE that's already juggling message counts, unread badges, last-message previews, and participant lookups — all in a single RPC call.",[57,751,752],{},"This is a post about how that goes, what breaks, and what actually works.",[76,754,756],{"id":755},"offset-pagination-the-tempting-lie","OFFSET pagination: the tempting lie",[57,758,759],{},"The first version always uses OFFSET. It always does. OFFSET is the pagination equivalent of duct tape — it works, it's fast to write, and it will absolutely embarrass you in production.",[57,761,762],{},"The problem is that PostgreSQL still scans and discards all the skipped rows on every single request. At small scale, you don't notice. At real scale, query time grows linearly with page number. Page 1 is fast. Page 500 is a customer complaint.",[57,764,765],{},"There's also the classic \"ghost row\" bug: if a new record is inserted between page 1 and page 2 fetches, your offset shifts. The user either sees a duplicate or silently skips a row. The data is lying to you and the frontend has no idea.",[76,767,769],{"id":768},"cursor-based-pagination-the-right-answer-with-sharp-edges","Cursor-based pagination: the right answer (with sharp edges)",[57,771,772],{},"Cursor pagination solves both problems. Instead of \"give me rows 40–60,\" you say \"give me rows that come after this specific row.\" Stable, consistent, and index-friendly regardless of how deep you go.",[57,774,775],{},"The cursor is typically the value you're ordering by, combined with a unique ID as a tiebreaker. This matters more than you'd think — two records can absolutely have the same timestamp, and databases will find that edge case on a Friday afternoon when you least want them to.",[57,777,778],{},"PostgreSQL's row value comparisons are clean for this:",[780,781,785],"pre",{"className":782,"code":783,"language":784,"meta":198,"style":198},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","WHERE (ordered_at, id) \u003C (cursor_ordered_at, cursor_id)\n","sql",[64,786,787],{"__ignoreMap":198},[788,789,792],"span",{"class":790,"line":791},"line",1,[788,793,783],{},[57,795,796],{},"This hits your index correctly and stays fast whether you're on page 1 or page 1000. Cursor pagination doesn't degrade. That's the whole point.",[76,798,800],{"id":799},"structuring-complex-ctes-for-pagination","Structuring complex CTEs for pagination",[57,802,803],{},"When a query has multiple concerns — aggregations, joins, computed fields — CTEs are the natural way to break it apart. But where you apply the pagination filter matters enormously.",[57,805,806],{},"The instinct is to filter early: \"let me cut the dataset down first, then do the rest.\" This is wrong. If you filter before computing aggregations, your counts and summaries are based on the filtered set, not the full one. You end up with correct-looking but subtly wrong numbers.",[57,808,809],{},"The right pattern is to compute everything first, then paginate at the last step. Structure it roughly like:",[677,811,812,818,824],{},[295,813,814,817],{},[86,815,816],{},"Base CTE"," — filter to the relevant scope (e.g. records for this user)",[295,819,820,823],{},[86,821,822],{},"Enrichment CTEs"," — joins, aggregations, computed fields",[295,825,826,829,830,833],{},[86,827,828],{},"Final CTE"," — apply the cursor filter and ",[64,831,832],{},"LIMIT"," here, not before",[57,835,836],{},"This keeps your aggregations honest and your pagination clean.",[76,838,840],{"id":839},"getting-total-count-without-a-second-query","Getting total count without a second query",[57,842,843],{},"A common pattern is to fire two queries: one for the page of data, one for the total count (so the frontend knows whether there's a next page). That works, but it's an extra round trip you don't need.",[57,845,846,847,850],{},"PostgreSQL's window functions handle this elegantly. Adding ",[64,848,849],{},"COUNT(*) OVER()"," to your final SELECT gives you the total matching rows alongside each result row — no separate query, no extra latency. One call, full information.",[57,852,853],{},"It's one of those features that feels almost too convenient when you first discover it.",[76,855,857],{"id":856},"the-distinct-on-trap","The DISTINCT ON trap",[57,859,860,861,864],{},"If you're using ",[64,862,863],{},"DISTINCT ON"," to get the \"latest record per group\" — a common pattern for things like last message per conversation — there's a subtle ordering rule that will burn you at least once.",[57,866,867,868,871,872,874,875,877],{},"The ",[64,869,870],{},"ORDER BY"," clause inside a ",[64,873,863],{}," query must lead with the ",[64,876,863],{}," column. If your outer query needs results sorted differently, you have to wrap it in a subquery and re-sort outside. PostgreSQL won't warn you when you get this wrong. It'll just return data that looks right but isn't.",[57,879,880,881,883],{},"Always verify ",[64,882,863],{}," results with multi-record test data. Single-record test cases hide this bug completely.",[76,885,887],{"id":886},"test-with-multiple-users-early","Test with multiple users early",[57,889,890],{},"This applies to anything with per-user state — unread counts, permissions, personalized feeds. Single-user testing will give you false confidence.",[57,892,893],{},"The bugs in per-user aggregations only appear when two or more users are actually interacting with the same data simultaneously. If you're not testing that, you're testing a much simpler system than the one you've actually built.",[57,895,896],{},"Set up proper multi-user test fixtures early. It's annoying to do and extremely worth it.",[76,898,900],{"id":899},"lessons-learned","Lessons learned",[292,902,903,906,909,912,917,922],{},[295,904,905],{},"OFFSET is fine for internal tools. For user-facing features at scale, use cursors.",[295,907,908],{},"Always include a unique tiebreaker in your cursor. Timestamps are not unique.",[295,910,911],{},"Apply pagination filters at the final CTE step, not the first.",[295,913,914,916],{},[64,915,863],{}," has ordering constraints. Read them carefully.",[295,918,919,921],{},[64,920,849],{}," eliminates the need for a separate count query.",[295,923,924],{},"Test aggregations with real multi-user scenarios from the start.",[57,926,927],{},"The query that comes out the other side of this process isn't always pretty. But it's correct, fast, and won't wake you up at 3am. In production database work, that's basically the win condition.",[929,930,931],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":198,"searchDepth":199,"depth":199,"links":933},[934,935,936,937,938,939,940],{"id":755,"depth":199,"text":756},{"id":768,"depth":199,"text":769},{"id":799,"depth":199,"text":800},{"id":839,"depth":199,"text":840},{"id":856,"depth":199,"text":857},{"id":886,"depth":199,"text":887},{"id":899,"depth":199,"text":900},"2026-05-20","OFFSET feels right and embarrasses you in production. A practical look at cursor pagination, structuring CTEs honestly, and the DISTINCT ON trap, drawn from a real chat app.","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F546819\u002Fpexels-photo-546819.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},{"title":18,"description":942},"-XNhQP_Cvqd2lxGInWCzr1gPykGM4Vt_9yw4hP2_lcA",{"id":948,"title":26,"author":949,"body":951,"date":1301,"description":1302,"extension":209,"image":1303,"meta":1304,"minRead":1124,"navigation":41,"path":27,"seo":1305,"stem":28,"__hash__":1306},"blog\u002Fblog\u002Ftaming-supabase-realtime-in-a-tauri-app.md",{"name":48,"username":49,"to":50,"avatar":950},{"src":52,"alt":48},{"type":54,"value":952,"toc":1295},[953,956,959,962,965,969,976,979,983,986,989,1001,1005,1008,1013,1020,1148,1151,1156,1159,1265,1268,1270,1284,1287,1292],[57,954,955],{},"So you've built a beautiful desktop app with Tauri. It's fast, it's native-ish, and you've wired up Supabase Realtime to push live updates straight to your users. It works great in dev. You demo it, everyone claps. You ship it.",[57,957,958],{},"Then someone closes their laptop lid.",[57,960,961],{},"Here's the thing nobody puts in the getting started guide: Supabase Realtime and laptop sleep modes have the kind of relationship that software engineers describe as \"undefined behavior.\" The WebSocket connection drops when the machine sleeps, and when it wakes up, your channel just sits there — technically alive, spiritually gone. No error. No reconnect. Just silence. It's the software equivalent of a colleague who stopped responding to Slack three hours ago but their status still says \"Active.\"",[57,963,964],{},"I ran into this building The-Tie-Bridge, a Tauri v2 desktop app backed by Supabase. Here's what I learned.",[76,966,968],{"id":967},"the-problem-with-webview-focus","The problem with WebView focus",[57,970,971,972,975],{},"Tauri wraps your frontend in a WebView, and WebView has its own lifecycle that doesn't always map neatly to what you'd expect. When the app loses focus — or the machine sleeps — the underlying WebSocket can go stale. The tricky part is that Supabase's client doesn't always ",[387,973,974],{},"know"," it's stale. It might even attempt to reconnect on its own, but in the process, it misses events that fired while it was out. Those events are just gone. No replay, no buffer.",[57,977,978],{},"This is not a bug in Supabase. It's just the reality of long-lived WebSocket connections in a desktop environment that has power management, focus states, and a WebView runtime that isn't a full browser.",[76,980,982],{"id":981},"the-wrong-fix-that-feels-right","The wrong fix (that feels right)",[57,984,985],{},"The first instinct is to add a reconnect loop. Just check every N seconds if the channel is still alive and re-subscribe if not. Simple enough, right?",[57,987,988],{},"Except:",[677,990,991,998],{},[295,992,993,994,997],{},"You're now polling inside a ",[64,995,996],{},"setInterval",", which — surprise — is not reliable across sleep\u002Fwake cycles either. The interval can fire late, early, or get batched. JavaScript timers in a sleeping WebView are basically on vacation.",[295,999,1000],{},"Even if you do reconnect, you've already missed anything that happened during the gap. Your UI is now silently stale. Congratulations, you've built an eventually-inconsistent desktop app. (Distributed systems engineers are nodding knowingly. Everyone else will be shortly.)",[76,1002,1004],{"id":1003},"the-hybrid-approach-that-actually-works","The hybrid approach that actually works",[57,1006,1007],{},"The solution that held up in production is a combination of two things.",[57,1009,1010],{},[86,1011,1012],{},"1. Use Tauri's native window events, not JS timers.",[57,1014,1015,1016,1019],{},"Tauri exposes ",[64,1017,1018],{},"appWindow.onFocusChanged()"," — a native-level listener that fires reliably when the window regains focus, regardless of what the JS runtime was doing while the app was sleeping. This is your trigger to revalidate the Realtime connection.",[780,1021,1025],{"className":1022,"code":1023,"language":1024,"meta":198,"style":198},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { appWindow } from \"@tauri-apps\u002Fapi\u002Fwindow\";\n\nappWindow.onFocusChanged(({ payload: focused }) => {\n  if (focused) {\n    checkAndReconnectChannels();\n  }\n});\n","typescript",[64,1026,1027,1060,1065,1104,1122,1133,1138],{"__ignoreMap":198},[788,1028,1029,1033,1037,1041,1044,1047,1050,1054,1057],{"class":790,"line":791},[788,1030,1032],{"class":1031},"s7zQu","import",[788,1034,1036],{"class":1035},"sMK4o"," {",[788,1038,1040],{"class":1039},"sTEyZ"," appWindow",[788,1042,1043],{"class":1035}," }",[788,1045,1046],{"class":1031}," from",[788,1048,1049],{"class":1035}," \"",[788,1051,1053],{"class":1052},"sfazB","@tauri-apps\u002Fapi\u002Fwindow",[788,1055,1056],{"class":1035},"\"",[788,1058,1059],{"class":1035},";\n",[788,1061,1062],{"class":790,"line":199},[788,1063,1064],{"emptyLinePlaceholder":41},"\n",[788,1066,1068,1071,1073,1077,1080,1083,1087,1090,1094,1097,1101],{"class":790,"line":1067},3,[788,1069,1070],{"class":1039},"appWindow",[788,1072,474],{"class":1035},[788,1074,1076],{"class":1075},"s2Zo4","onFocusChanged",[788,1078,1079],{"class":1039},"(",[788,1081,1082],{"class":1035},"({",[788,1084,1086],{"class":1085},"swJcz"," payload",[788,1088,1089],{"class":1035},":",[788,1091,1093],{"class":1092},"sHdIc"," focused",[788,1095,1096],{"class":1035}," })",[788,1098,1100],{"class":1099},"spNyl"," =>",[788,1102,1103],{"class":1035}," {\n",[788,1105,1107,1110,1113,1116,1119],{"class":790,"line":1106},4,[788,1108,1109],{"class":1031},"  if",[788,1111,1112],{"class":1085}," (",[788,1114,1115],{"class":1039},"focused",[788,1117,1118],{"class":1085},") ",[788,1120,1121],{"class":1035},"{\n",[788,1123,1125,1128,1131],{"class":790,"line":1124},5,[788,1126,1127],{"class":1075},"    checkAndReconnectChannels",[788,1129,1130],{"class":1085},"()",[788,1132,1059],{"class":1035},[788,1134,1135],{"class":790,"line":212},[788,1136,1137],{"class":1035},"  }\n",[788,1139,1140,1143,1146],{"class":790,"line":348},[788,1141,1142],{"class":1035},"}",[788,1144,1145],{"class":1039},")",[788,1147,1059],{"class":1035},[57,1149,1150],{},"This fires when the user alt-tabs back, opens the lid, or brings the window to the front. It's rock solid because it's operating at the OS level, not the JS event loop level.",[57,1152,1153],{},[86,1154,1155],{},"2. Back it up with a lightweight heartbeat + background poll.",[57,1157,1158],{},"Realtime handles the live updates. But for any data that might have changed while the connection was dead, fire a one-time fetch on focus restore. Think of it as the app \"catching up\" before trusting the live stream again.",[780,1160,1162],{"className":1022,"code":1161,"language":1024,"meta":198,"style":198},"async function checkAndReconnectChannels() {\n  const isConnected = await pingRealtimeChannel();\n\n  if (!isConnected) {\n    await resubscribeAllChannels();\n  }\n\n  \u002F\u002F Fetch any missed updates regardless\n  await syncLatestState();\n}\n",[64,1163,1164,1179,1200,1204,1220,1232,1236,1240,1246,1259],{"__ignoreMap":198},[788,1165,1166,1169,1172,1175,1177],{"class":790,"line":791},[788,1167,1168],{"class":1099},"async",[788,1170,1171],{"class":1099}," function",[788,1173,1174],{"class":1075}," checkAndReconnectChannels",[788,1176,1130],{"class":1035},[788,1178,1103],{"class":1035},[788,1180,1181,1184,1187,1190,1193,1196,1198],{"class":790,"line":199},[788,1182,1183],{"class":1099},"  const",[788,1185,1186],{"class":1039}," isConnected",[788,1188,1189],{"class":1035}," =",[788,1191,1192],{"class":1031}," await",[788,1194,1195],{"class":1075}," pingRealtimeChannel",[788,1197,1130],{"class":1085},[788,1199,1059],{"class":1035},[788,1201,1202],{"class":790,"line":1067},[788,1203,1064],{"emptyLinePlaceholder":41},[788,1205,1206,1208,1210,1213,1216,1218],{"class":790,"line":1106},[788,1207,1109],{"class":1031},[788,1209,1112],{"class":1085},[788,1211,1212],{"class":1035},"!",[788,1214,1215],{"class":1039},"isConnected",[788,1217,1118],{"class":1085},[788,1219,1121],{"class":1035},[788,1221,1222,1225,1228,1230],{"class":790,"line":1124},[788,1223,1224],{"class":1031},"    await",[788,1226,1227],{"class":1075}," resubscribeAllChannels",[788,1229,1130],{"class":1085},[788,1231,1059],{"class":1035},[788,1233,1234],{"class":790,"line":212},[788,1235,1137],{"class":1035},[788,1237,1238],{"class":790,"line":348},[788,1239,1064],{"emptyLinePlaceholder":41},[788,1241,1242],{"class":790,"line":738},[788,1243,1245],{"class":1244},"sHwdD","  \u002F\u002F Fetch any missed updates regardless\n",[788,1247,1249,1252,1255,1257],{"class":790,"line":1248},9,[788,1250,1251],{"class":1031},"  await",[788,1253,1254],{"class":1075}," syncLatestState",[788,1256,1130],{"class":1085},[788,1258,1059],{"class":1035},[788,1260,1262],{"class":790,"line":1261},10,[788,1263,1264],{"class":1035},"}\n",[57,1266,1267],{},"This hybrid gives you the best of both worlds: real-time updates when everything is healthy, and a reliable fallback that fills the gap when it isn't. Your users get a consistent experience without knowing any of this chaos is happening underneath.",[76,1269,900],{"id":899},[292,1271,1272,1275,1278,1281],{},[295,1273,1274],{},"Never trust JS timers for reconnection logic in a desktop app. They lie.",[295,1276,1277],{},"Tauri's native event listeners are underused and extremely reliable.",[295,1279,1280],{},"Treat sleep\u002Fwake as a mini-deployment: assume state is stale and verify.",[295,1282,1283],{},"The WebView is not a browser tab. Stop treating it like one.",[57,1285,1286],{},"If you're building anything with Tauri + Supabase Realtime, add the focus listener on day one. Future you — the one debugging a prod issue at 11pm because a user's chat stopped updating after their standup — will be grateful.",[57,1288,1289],{},[387,1290,1291],{},"Written from real scars. The-Tie-Bridge is a Tauri v2 + Supabase desktop app. The reconnection bugs described above were not hypothetical.",[929,1293,1294],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":198,"searchDepth":199,"depth":199,"links":1296},[1297,1298,1299,1300],{"id":967,"depth":199,"text":968},{"id":981,"depth":199,"text":982},{"id":1003,"depth":199,"text":1004},{"id":899,"depth":199,"text":900},"2026-05-13","Supabase Realtime and laptop sleep modes have a complicated relationship. Here's the hybrid reconnection approach that actually held up in production for a Tauri v2 desktop app.","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F17489151\u002Fpexels-photo-17489151.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},{"title":26,"description":1302},"FEsJtoMpL6PazbrgCARjCX7a31zK63G3OXsM8VBKuQQ",1782657704081]