[{"content":"A bug report has one job: give someone else enough information to reproduce the problem without asking follow-up questions. If the engineer has to come back and say \u0026ldquo;what browser were you using?\u0026rdquo; or \u0026ldquo;can you show me what you clicked?\u0026rdquo;, the report didn\u0026rsquo;t do its job.\nWhen you need this # Bug reports come in as vague one-liners (\u0026ldquo;the page is broken\u0026rdquo;) and engineers spend more time investigating what\u0026rsquo;s wrong than fixing it. The same bug gets reported multiple times because previous reports were too unclear to match against. Engineers can\u0026rsquo;t reproduce issues and end up closing tickets with \u0026ldquo;works for me.\u0026rdquo; What each section is for # Summary - One or two sentences. What\u0026rsquo;s broken and where? This is what someone reads when scanning a list of bugs to triage.\nSteps to reproduce - The most important section. Write this as if the reader has never seen the product before. Numbered steps, specific URLs, specific inputs. If you can\u0026rsquo;t reproduce it reliably, say so and describe what you were doing when it happened.\nCurrent behavior - What actually happens when you follow the steps. \u0026ldquo;I see a 500 error\u0026rdquo; or \u0026ldquo;the button does nothing\u0026rdquo; or \u0026ldquo;the page loads but the data is from yesterday.\u0026rdquo;\nExpected behavior - What should happen instead. This sounds obvious, but it forces the reporter to articulate the gap clearly. Sometimes writing this down reveals that the \u0026ldquo;bug\u0026rdquo; is actually a feature request.\nLogs and screenshots - Paste relevant console output, error messages, or screenshots. Use code blocks for logs. A screenshot of a blank page isn\u0026rsquo;t helpful; a screenshot showing the error message is.\nPossible fixes - Optional. If the reporter knows where the problem might be, linking to the relevant code saves the engineer investigation time. If you don\u0026rsquo;t know, skip this section entirely.\nThe template # ### Summary \u0026gt; Summarize the bug encountered concisely ### Steps to reproduce \u0026gt; How one can reproduce the issue - this is very important ### What is the current *bug* behavior? \u0026gt; What actually happens ### What is the expected *correct* behavior? \u0026gt; What you should see instead ### Relevant logs and/or screenshots \u0026gt; Paste any relevant logs - please use code blocks to format console output, \u0026gt; logs, and code as it\u0026#39;s tough to read otherwise. ### Possible fixes \u0026gt; If you can, link to the line of code that might be responsible for the problem ","externalUrl":null,"permalink":"/handbook/templates/bug-report/","section":"Dev Handbook","summary":"A bug report has one job: give someone enough info to reproduce the problem without asking follow-up questions.","title":"🐛 Bug Reports: Make It Reproducible","type":"handbook"},{"content":"One morning I start my car and notice a repeating knocking noise coming from the engine. Everything else seems to be working fine and the car seems drivable, so I look up a mechanic nearby to drop my car off. I need to get to work, so I look for a mechanic nearby that I can drop my car off, and they can fix it while I\u0026rsquo;m at work. When I arrive at the mechanic, they ask me to describe the problem with my vehicle. They ask when the noise started, the last time I had the car in for inspection, and they have me walk them around the vehicle. Before I hand over the keys, I mention\n- \u0026ldquo;This is my ride home, do you think the car will be ready by end of today?\u0026rdquo;\n- \u0026ldquo;We won\u0026rsquo;t know for sure until we get the car in the shop, but at first glance we should have it done by lunch.\u0026rdquo; the mechanic replies\n- \u0026ldquo;Great! Also, how much do you think this will set me back?\u0026rdquo; I ask.\n- \u0026ldquo;Well that is harder to say. I\u0026rsquo;ll give you a call before we start any work, so you know what the damage will be. Sound good?\u0026rdquo;\n- \u0026ldquo;Yeah that\u0026rsquo;s perfect. Thanks again for fitting me in on short notice.\u0026rdquo;\nWho are you in this story? # Mechanics are the Engineers Customers are the Stakeholders Identifying Expectations # What are my expectations of the mechanic? # Fix the car. Call me with an estimate before starting any work. Done early (by lunchtime) which is way before my ideal deadline (end of my workday). What are the mechanic\u0026rsquo;s expectations? # The problem I described to them is \u0026ldquo;in the ballpark\u0026rdquo; of the actual problem. I will answer the phone when they call with the estimate. Takeaway: Know what is expectedExpectations are both explicit and implicit. Keep an eye out for both kinds. Write them down and if you are unsure, ask the other parties if you understood them correctly. Estimating work # The noise my car is making is a common issue the Mechanic has seen several times before. Does he take the car into the back to open the car up and give me an estimate? No. He does some quick math and quotes me $500 and 4 hours to fix it.\nIf my car was an EV that he\u0026rsquo;s never seen before. They\u0026rsquo;ll tell me they need to take it into the back to run a diagnostic. It\u0026rsquo;ll cost them $100 in labor to do that, and then follow up with me on an estimate and timeline for a fix.\nTakeaway: Your word is your bondEstimation is a tool that is necessary in some cases and overkill in others. What matters is that what you deliver is accurate. If you\u0026rsquo;re confident, skip unnecessary steps. Just know that I am going to expect to pay $500 and pickup the car in 4 hours if that is what was the estimate I was given. Negotiating deliverables # Now think about both sides of this relationship: customer and mechanic. Both of us are the stakeholders in the same project which is \u0026ldquo;getting my car fixed\u0026rdquo;. Each of us have choices we can make without impacting the other. The mechanic can change how he is going to fix the car, they can also decide how much they want to charge me for the work. I can decide whether I want to pay that amount or not, I can decide to pick my car up early.\nWhat I cannot do is tell the mechanic how I want him to fix it or how much they should charge me. Could I make a suggestion? Definitely! Can I demand it? No. The mechanic has similar limitations in the decisions they are allowed to make.\nTakeaway: Compromise, but don\u0026#39;t compromise your credibilityThe customer is bringing you the work. It is the job of the mechanic to give a confidence estimate on how much it will cost and how long it will take. This boundary should be respected, and it will create healthy tensions. The customer may push to get it done sooner or cheaper. The mechanic can try to adapt the work to fit these requests, but at the end of the day the car still must be safe and reliable to drive. If the mechanic allows themselves to deliver a vehicle that is unsafe to drive due to cutting corners they\u0026rsquo;re out of business (and likely to spend years in court). No one asks them what the circumstances were that forced the mechanic to deliver a dangerous vehicle. Delivery # When the mechanic hands me the keys to my car, let\u0026rsquo;s say he gives me two different responses:\nYour car is all ready. Thanks for your business and hope you think of us next time! Your car is all ready. Before you go on any main roads, do you mind driving it around a bit and make sure it\u0026rsquo;s working ok? Which response instills more confidence? Response 1.\nWhy not Response 2? One could argue that Response 2 is a mechanic trying to deliver better customer service and make sure the customer is satisfied with their work. Yes that may be what the mechanic intended. However, can you see how the customer may be a bit concerned if things were done properly or to a high standard? The customer may be saying to themselves \u0026ldquo;I\u0026rsquo;m not a mechanic, I don\u0026rsquo;t even know what to look for! Isn\u0026rsquo;t that what I paid you to take care of for me?\u0026rdquo;\nTakeaway: Done means doneWhen delivering valuable work, take the time to make sure it\u0026rsquo;s done. Don\u0026rsquo;t force the customer to do the QA that you could have done. Don\u0026rsquo;t raise undue concern to the customer by asking them to \u0026ldquo;double check\u0026rdquo; something you already did. If you truly concerned about it, check the work a second, third or fourth time yourself before telling the customer that it\u0026rsquo;s \u0026ldquo;done\u0026rdquo;. Where to go next # The mechanic story introduces four ideas that the rest of this handbook builds on:\nExpectations are both explicit and implicit. Remote teams make this harder because you lose body language and hallway conversations. See Going Remote: Replacing What the Office Gave You for Free for how to fill that gap. Estimation is a tool for setting expectations, not a promise etched in stone. See From Problem to Feature: Scoping Work and Estimating: Measuring Risk, Not Time for the mechanics. Negotiating deliverables is the art of scoping work so both sides are happy. See From Problem to Feature: Scoping Work for how to break problems into well-sized features. Done means done is the most abused phrase in project management. See Your Project Board is a Mirror for how to make it mean something on your board. ","externalUrl":null,"permalink":"/handbook/story/","section":"Dev Handbook","summary":"A relatable analogy that frames the rest of the handbook: expectations, estimation, negotiation, and what ‘done’ really means.","title":"📖 The Mechanic Parable: A Story About Expectations","type":"handbook"},{"content":"Standup is not a status report. It\u0026rsquo;s a daily alignment check. The goal is to make sure the team is focused on the same thing and to surface anything that\u0026rsquo;s getting in the way.\nIf your standup feels like everyone is taking turns reading their to-do list to a manager, something is broken. A good standup is short, focused, and ends with the team more aligned than they were five minutes ago.\nWhen you need this # People are working on the wrong things, or on things that were quietly deprioritized. Blockers sit unresolved for days because nobody knew about them. Stakeholders or the team lead keep pinging individuals for status updates throughout the day. The team feels disconnected, especially on remote teams where there\u0026rsquo;s no hallway chatter to fill the gaps. Standup can accidentally slow communication downOnce a team adopts standup, a subtle habit can form. Someone finishes a ticket, hits a blocker, or makes a decision that affects the team, but instead of sharing it right away they think \u0026ldquo;I\u0026rsquo;ll just mention it in standup tomorrow.\u0026rdquo; Nobody decides to do this. It just happens organically. The mere existence of a daily sync gives people a reason to wait, and waiting feels reasonable because \u0026ldquo;there\u0026rsquo;s a meeting for that.\u0026rdquo; The standup becomes a reason to delay communication instead of improving it.\nThis directly undermines the work you\u0026rsquo;re doing to keep the project board reflecting reality. If someone finishes a ticket at 2pm but doesn\u0026rsquo;t update the board or tell anyone until standup the next morning, the board is lying for 18 hours. Multiply that across the team and you\u0026rsquo;ve got a board that\u0026rsquo;s always a day behind.\nStandup is for alignment and blockers, not for breaking news. Updates to the board and quick Slack messages should happen in real time. If someone\u0026rsquo;s standup update is always \u0026ldquo;I already posted this in Slack yesterday,\u0026rdquo; that\u0026rsquo;s a good sign, not a bad one.\nWho should be there? # At startups, this can be company-wide. At larger companies it should be focused on the people building the thing, plus the team lead. If someone is in the standup but never speaks or never gets value from it, they probably shouldn\u0026rsquo;t be there. Keep the group tight.\nThe format # 1. Restate the goal # Every standup opens with the same thing: the goal for the week.\n(As a reminder,) the goal(s) for this week are: ____\nThis should not change day to day. Repeating it is the point. It\u0026rsquo;s a forcing function to keep the team focused as a group, especially on remote teams where it\u0026rsquo;s easy to drift into solo mode.\nIf the goal changed since yesterday, say so explicitly and explain why. Don\u0026rsquo;t just quietly swap it out.\n2. Individual updates # Each person answers three questions:\nAnything notable to share from yesterday? What are you working on today? Any blockers? A couple of things to keep in mind:\n\u0026ldquo;Anything notable from yesterday\u0026rdquo; is often nothing. That\u0026rsquo;s fine. Don\u0026rsquo;t force people to narrate their whole day. If they made progress on their ticket and nothing unexpected happened, a quick \u0026ldquo;nothing to add, still on the same ticket\u0026rdquo; is a perfectly good update. The goal is to surface surprises, not to prove you were productive.\nBlockers are the most important part. A blocker is anything preventing someone from making progress: waiting on input from another person, waiting on a PR review, a decision that hasn\u0026rsquo;t been made, time off that\u0026rsquo;s eating into their capacity. When someone raises a blocker, the team lead\u0026rsquo;s job is to make sure it gets resolved today, not \u0026ldquo;soon.\u0026rdquo;\n3. Wrap up # Look at the board together. Adjust any assignments to better align with the week\u0026rsquo;s goal. If someone finished their ticket, make sure they know what to pick up next.\nThis is also the moment to catch WIP limit violations. If three tickets are \u0026ldquo;In Progress\u0026rdquo; on a team of four, ask why.\nCommon problems # Standup takes too long. It should be under 10 minutes for a team of 4-6 people. If it\u0026rsquo;s running longer, people are either giving too much detail, having side conversations, or problem-solving in the standup itself. Side conversations should be taken offline: \u0026ldquo;let\u0026rsquo;s sync on that after standup.\u0026rdquo;\nNobody mentions blockers. This usually means people don\u0026rsquo;t feel safe raising them, or they\u0026rsquo;ve learned that raising a blocker doesn\u0026rsquo;t lead to action. If blockers consistently go unresolved, people stop mentioning them. The team lead needs to follow through visibly.\nThe goal never changes. If the weekly goal is the same for three weeks straight, it\u0026rsquo;s either too vague (\u0026ldquo;make progress on the project\u0026rdquo;) or the team is stuck and not talking about it.\nPeople skip standup regularly. On remote teams this is a signal that standup isn\u0026rsquo;t providing value. Before mandating attendance, ask why people are skipping. The answer usually points to a format problem, not a discipline problem.\nTeam BuildingIf you are a new team it may be a good idea to start standup with an icebreaker. I like to use a \u0026ldquo;question of the day\u0026rdquo; that folks answer as part of their turn in standup. It adds 30 seconds per person and goes a long way toward building rapport on a remote team.\nWhat was your favorite vacation spot growing up? What did you want to be as a kid? Which sport is your LEAST favorite to watch? ","externalUrl":null,"permalink":"/handbook/rituals/standups/","section":"Dev Handbook","summary":"A short daily check-in to surface blockers and keep the team focused on the same goal. Not a status report.","title":"🧍 Standups: Daily Alignment, Not Status Reports","type":"handbook"},{"content":"Switching from a traditional in-office job to a remote job puts your habits and skills to the test. Positive habits developed in an office setting often have negative results in a remote setting.\nThings like:\n\u0026ldquo;Just get it done\u0026rdquo; \u0026ldquo;I want to see results\u0026rdquo; \u0026ldquo;Can you help me sometime later today?\u0026rdquo; \u0026ldquo;I\u0026rsquo;ll grab you when I can\u0026rdquo; Each of these has been encouraged by employers as ways to get ahead and be an effective team member. But since going remote, each of these habits has gone through a systematic process to undo them. This page explains why, and what to replace them with.\n\u0026ldquo;Just get it done\u0026rdquo; # My fictional boss Stephen comes in with his morning coffee and on his way to his office says, \u0026ldquo;I have a project I need done, meet me in my office in five minutes.\u0026rdquo; I grab my notebook and head into his office to find out more. The next thirty minutes are spent covering a new initiative that needs to be addressed by next week. If I am to do it right I know I better be thorough before stepping out of the meeting less I forget an important detail.\nStarting out my career as a junior developer I yearned for the opportunity to do the \u0026ldquo;fun pet projects\u0026rdquo; that my senior peers worked on. I\u0026rsquo;d pressure my boss with a \u0026ldquo;why not me?\u0026rdquo; argument whenever the opportunity presented itself. It took me a few years to realize what made my co-workers \u0026ldquo;pet project worthy\u0026rdquo; was they \u0026ldquo;just got it done.\u0026rdquo;\nTaking on this new \u0026ldquo;just get it done\u0026rdquo; stance at the office worked out great. I quickly became the go-to employee for the majority of projects. However, when I switched to a remote position, this way of working became detrimental to my career and success at the company.\nThe office gave you observational communication for free # There is a major difference between being in an office environment and a remote environment: people can see you. I know that\u0026rsquo;s obvious, but let it sink in for a minute.\nThe \u0026ldquo;just get it done\u0026rdquo; approach exchanges direct communication for observational communication. Instead of nagging my boss for clarifications and additional requirements, we communicated progress by observing: staying late, being the first one in, having working lunches. He knew, or more accurately he assumed, that every step of the way I was busting my ass to get the project done. He didn\u0026rsquo;t come ask me; he judged by what he saw. This observational communication built a level of trust between us that was pivotal for the whole system to work.\nMy boss seeing me in my seat doesn\u0026rsquo;t mean I\u0026rsquo;m going to make the deadline or that I\u0026rsquo;m not struggling. That\u0026rsquo;s not the point. The fact is, this is how it worked. He was reading my body language and used that to reaffirm his assumptions. It\u0026rsquo;s not ideal, but it\u0026rsquo;s the reality of office work.\nRemote work removes all of that # In a remote environment, body language goes out the window. Even on video you often can\u0026rsquo;t trust it due to latency or other technical delays. All of the observational communication mechanisms that worked in-office no longer function.\nBut our habits still drive us to look for observational signals. If I need a status update on a project, I instinctively go check emails, chat rooms, even GitHub repos for \u0026ldquo;signs of life.\u0026rdquo; This is my virtual way of searching the office for the person who isn\u0026rsquo;t at their desk.\nThe fastest way to lose trustRegardless of environment, there is no faster way to lose trust in someone than when you need answers and they are nowhere to be found. In an office, at least you can see the empty chair and draw your own conclusions. Remote, there\u0026rsquo;s nothing. Silence reads as absence, and absence reads as disengagement. Replace \u0026ldquo;just get it done\u0026rdquo; with \u0026ldquo;notify and handle it\u0026rdquo; # Folks working in a remote environment have a shared responsibility to each other. They must fill the void left by the lack of observational communication with written communication. This means replacing the \u0026ldquo;just get it done\u0026rdquo; approach with a \u0026ldquo;notify and handle it\u0026rdquo; approach.\nThe adjustment is actively communicating events as they come up and handling them accordingly.\nNotify # By \u0026ldquo;event\u0026rdquo; I mean anything of interest, planned or unplanned, setbacks and milestones. Actively communicating these things builds trust all around. It instills confidence that nothing is being missed or falling through the cracks.\nHere\u0026rsquo;s what this looks like in practice. Say I hit an unexpected infrastructure issue. Instead of going heads down and quietly solving it, I send a quick message:\nTeam, I ran into an unexpected issue with firewall rules on one of our web nodes that will prevent us from using web sockets safely. I\u0026rsquo;ve looked at our options and suggest we set up some boxes in a DMZ zone to bypass this issue while maintaining security of the existing servers. Please let me know if there\u0026rsquo;s any issue with this approach or if you want more detail. I\u0026rsquo;d be happy to set something up.\nThat took two minutes to write. It tells the team what happened, what I\u0026rsquo;m going to do about it, and gives them a window to weigh in before I start.\nHandle it # The \u0026ldquo;just get it done\u0026rdquo; part doesn\u0026rsquo;t go away. When you\u0026rsquo;re assigned something, you still do whatever it takes to get it done. The difference is the notification step it\u0026rsquo;s paired with. After sending that message, I\u0026rsquo;d usually give it an hour before jumping on the solution, just in case I misunderstood something or someone has a better idea. Then I\u0026rsquo;m off to implement.\nThis isn\u0026rsquo;t an exact science. It\u0026rsquo;s understanding that when you\u0026rsquo;re remote, you must keep folks abreast of what you\u0026rsquo;re working on. Without it, people begin to wonder if you\u0026rsquo;re stuck or heading off track. These quick messages serve as a reminder that you are neither of those things.\nOvercommunicate until it feels weirdWhen you first go remote, you\u0026rsquo;ll feel like you\u0026rsquo;re sharing too much. \u0026ldquo;Does anyone really care that I\u0026rsquo;m switching to a different ticket?\u0026rdquo; Yes. They do. In an office they would have seen you stand up, walk to the whiteboard, and move a sticky note. Remote, they see nothing unless you tell them. Err on the side of sharing more. You can always dial it back once the team finds its rhythm. This is why the rest of the handbook exists # Every practice in this handbook is, at some level, a tool for replacing observational communication with something intentional and written:\nYour Project Board is a Mirror because nobody can glance across the room to see what\u0026rsquo;s happening. The board has to tell that story instead. Standups exist to surface blockers and alignment issues that would have been caught in hallway conversations. But they can also accidentally slow communication down if people start saving updates for the meeting instead of sharing them in real time. Stories need clear acceptance criteria because you can\u0026rsquo;t lean over and ask \u0026ldquo;hey, what did you mean by this?\u0026rdquo; You need the ticket to stand on its own. Done means done matters more when nobody can see the loose ends piling up on your desk. If your team is struggling with any of these practices, it\u0026rsquo;s worth asking whether the root cause is a communication gap that the office used to fill for free.\n","externalUrl":null,"permalink":"/handbook/going-remote/","section":"Dev Handbook","summary":"Office habits break when you go remote. Here’s what to replace them with and why every other practice in this handbook depends on it.","title":"🌐 Going Remote: Replacing What the Office Gave You for Free","type":"handbook"},{"content":"A pull request description isn\u0026rsquo;t for you. It\u0026rsquo;s for the person reviewing your code. Their job is to verify that the changes are correct, safe, and complete. Your job is to make that as easy as possible.\nA PR with no description forces the reviewer to reverse-engineer your intent from the diff. That\u0026rsquo;s slow, error-prone, and frustrating for everyone involved.\nWhen you need this # Code reviews take too long because reviewers don\u0026rsquo;t understand the context of the changes. PRs get approved without meaningful review because the diff is too big to reason about without guidance. Engineers merge changes that don\u0026rsquo;t link back to a ticket, making it hard to trace why something was changed. What each section is for # Background - Why does this PR exist? Link to the ticket. If the ticket doesn\u0026rsquo;t tell the whole story (maybe the implementation approach diverged from what was planned), explain the technical context here. The reviewer should understand the \u0026ldquo;why\u0026rdquo; before they look at a single line of code.\nChanges - A bullet list of what you changed, in plain language. Not a commit-by-commit replay. Think of it as a map of the diff: \u0026ldquo;Added a new endpoint for X, refactored Y to support Z, removed the old migration.\u0026rdquo; This tells the reviewer where to focus.\nScreenshots - If the change has a visual component, show it. Before and after screenshots make UI changes trivial to review. Remove this section entirely if the change is backend-only; an empty screenshots section just adds noise.\nTODO - Anything that\u0026rsquo;s unfinished or needs follow-up. This is especially useful for draft PRs. If there are open questions, flag them here so the reviewer knows what\u0026rsquo;s settled and what\u0026rsquo;s still in flux.\nLink the ticket, every timeEvery PR should reference its ticket with Fixes #123 or equivalent. This creates a two-way link: the PR points to the ticket for context, and the ticket points to the PR for implementation details. Six months from now, when someone asks \u0026ldquo;why did we change this?\u0026rdquo;, that link is the answer. The template # ## Background \u0026lt;!-- Explain any background of why this PR is being made. Often a link to the main issue is enough, but you may need to elaborate on it to explain something technical. Fixes #{ticket_number} --\u0026gt; ## Changes \u0026lt;!-- A bullet list summarizing the changes you have made. --\u0026gt; * TBD ## Screenshots \u0026lt;!-- Remove if not relevant --\u0026gt; ## TODO \u0026lt;!-- Any items that need to be resolved still. Also, evaluate if a \u0026#34;Draft\u0026#34; PR and adding the \u0026#34;Work in Progress\u0026#34; label is appropriate. - [ ] Make sure to notify developers of change - [ ] Figure out what fixtures are necessary --\u0026gt; ","externalUrl":null,"permalink":"/handbook/templates/pull-requests/","section":"Dev Handbook","summary":"A PR description isn’t for you. It’s for the person reviewing your code. Make their job easy.","title":"🔀 Pull Requests: Tell Reviewers What to Look For","type":"handbook"},{"content":"Scoping is where most projects go wrong. Not because the team can\u0026rsquo;t build the thing, but because nobody agrees on what \u0026ldquo;the thing\u0026rdquo; is. This page covers how to break a problem down into features that are small enough to deliver, clear enough to estimate, and valuable enough to justify the work.\nThroughout this page I\u0026rsquo;ll use the word \u0026ldquo;feature\u0026rdquo; instead of \u0026ldquo;story.\u0026rdquo; It better illustrates how to organize work into logical pieces that each deliver value on their own.\nWhat is a feature? # A feature is a small set of changes that build up to solving a particular problem for a stakeholder.\nBreaking down into features # If we were building an authentication service, the features may break down to look like this:\nGood: Provides incremental value # Title: \u0026ldquo;Customers have the ability to log in\u0026rdquo; \u0026ldquo;When I go enter a username and password that is correct, I am logged in\u0026rdquo; \u0026ldquo;When I go enter a username and password that is incorrect, I see an error saying \u0026lsquo;invalid username or password\u0026rsquo;\u0026rdquo; \u0026ldquo;When skip entering a username or password, I see an error saying fields are required\u0026rdquo; Title: \u0026ldquo;Already logged in customers cannot visit login page\u0026rdquo; Title: \u0026ldquo;Customers are logged out after 24 hours of inactivity\u0026rdquo; Title: \u0026ldquo;Customers can manually log out\u0026rdquo; Title: \u0026ldquo;Employees are able to log in as customers\u0026rdquo; Each of these stories delivers a small amount of value to the larger project. Each feature stands on its own, meaning it provides value without being dependent on another.\nLet\u0026rsquo;s talk through some popular alternative ways to scope features:\nBad: Features are too big # In this scenario you may have just two features:\n\u0026ldquo;Customers can authenticate\u0026rdquo; \u0026ldquo;Customers can log in\u0026rdquo; \u0026ldquo;When I go enter a username and password that is correct, I am logged in\u0026rdquo; \u0026ldquo;When I go enter a username and password that is incorrect, I see an error saying \u0026lsquo;invalid username or password\u0026rsquo;\u0026rdquo; \u0026ldquo;When skip entering a username or password, I see an error saying fields are required\u0026rdquo; \u0026ldquo;Customers can log out\u0026rdquo; \u0026ldquo;Customers are logged out after 24 hours\u0026rdquo; … \u0026ldquo;Employees can log in\u0026rdquo; \u0026ldquo;Employees are able to log in as customers\u0026rdquo; … Issues # The tickets are too long and folks skip over them. They cover far too much surface area to be held in your brain. It is unclear what parts of the story are essential, nice-to-have, or optional. Testing every scenario is difficult as there is just so much to test. Smell: You need a matrix for every user scenario as part of the ticket. Too much needs to be built to show progress. Smell: The story will stall in \u0026ldquo;In Progress\u0026rdquo; for a long enough time, folks will ask for an update in stand-ups. Bad: Features that are just task lists # \u0026ldquo;Create login page\u0026rdquo; \u0026ldquo;Connect login page to Auth Service\u0026rdquo; \u0026ldquo;Add cancel button to go back to home page\u0026rdquo; \u0026ldquo;Guard login page from logged-in users\u0026rdquo; \u0026ldquo;Detect when customers inactive for 24 hours\u0026rdquo; \u0026ldquo;Log out inactive customers are 24 hours via CRON\u0026rdquo; Issues # Difficult to track progress: When can a user log in? When can they log out? Smell: Folks will want updates to \u0026ldquo;explain what this means\u0026rdquo; in the form of stand-ups or written updates. Difficult to know when \u0026ldquo;done means done\u0026rdquo;, when do we QA everything? Smell: Do you have a QA ticket? Any adjustment in implementation requires updating all tickets. Smell: Tickets say \u0026ldquo;Blocked by other ticket\u0026rdquo; Smell: Deliverables say \u0026ldquo;X will be done as part of other ticket\u0026rdquo; Stories must be worked in order. Smell: Engineer asks what to pick up next when the backlog is not empty. Their response is \u0026ldquo;I can\u0026rsquo;t until \\[…\\]\u0026rdquo; Engineering implementation is hiding product value Smell: Engineers cannot answer \u0026ldquo;Why is this important to the business?\u0026rdquo; on their own tickets. Their reasoning is coming from describing the implementation like \u0026ldquo;Without log out, we cannot test log in\u0026rdquo;. What goes into a feature? # A good story has clear answers to the following questions:\nWhat problem are we trying to solve for the business? How will we know when we solve the problem? What makes solving this problem worth solving? Exercise: 10 seconds to find your why # Are you not sure why a story needs to get done? Hover over the delete button. Give yourself to the count of 10 to come up with a reason why you need to do this or delete it. As you get closer to zero, panic sets in, and you\u0026rsquo;re primed to blurt out your answer: \u0026ldquo;Times up!\u0026rdquo; - \u0026ldquo;Wait! Without this though we\u0026rsquo;ll have no way to ever log someone out!\u0026rdquo; There you go, that\u0026rsquo;s why you need to do this work.\nExercise: How to know when it\u0026rsquo;s done # The feature is \u0026ldquo;Allow a user to log out manually\u0026rdquo;. How do we come up with a complete list of deliverables?\nImagine you are going to pay a total stranger to verify this problem was solved. You didn\u0026rsquo;t get to speak to them, they just had the ticket in front of them. What would you put as acceptance criteria assuming this person knew nothing of the implementation or systems?\nWould you write:\nThe authentication service destroys the cookie successfully after logging out Emit data to the security team\u0026rsquo;s audit log. OR would you write:\nAfter hitting the banner in the top of the landing page (https:…), I see a red banner like this (see Login Screenshot below). When logged out I can see the Login button When logged in I cannot see the Login button After logging out, when I visit the Security Team dashboard (https:…), I can identify an entry saying my user is now logged out. After logging out, restarting my browser does not log me back in. By using the latter format, you have a stable idea of what it takes to solve the problem. If the banner is missing, the user won\u0026rsquo;t know they are logged out, we need to reject the feature and keep working on it. It is also implementation independent. This means an engineer can make optimizations to their implementation as they see fit. These optimizations do not change the outcome of the feature. They only solve the problem in a better way.\nCommonly missed deliverables Notify a person or team about the change ahead of release. Get sign-off from someone before merging. Write public-facing documentation for this new feature. Draft a new feature in the icebox to perform a follow-up action. These all go into \u0026ldquo;done means done\u0026rdquo;, but are often omitted. Acceptance Criteria is what it takes to get something done and that usually includes some organizational pieces. Do yourself a favor and capture them.\nExercise: Mapping a problem to the business # Sticking with our feature \u0026ldquo;Allow a user to log out manually\u0026rdquo;. We\u0026rsquo;ve already discussed why we need this, \u0026ldquo;Without this we\u0026rsquo;ll have no way to ever log someone out!\u0026rdquo;, but what is the problem this solves for the business? Often you can ask \u0026ldquo;What value is added by solving this problem?\u0026rdquo;\nSome ideas might be:\n\u0026ldquo;Provide a way to safely access our site from a public computer\u0026rdquo; \u0026ldquo;Provide a way to switch user accounts\u0026rdquo; This is intentionally a trivial example. It illustrates that even the most trivial or the most engineering-specific feature requests can map to a problem and potential ROI (return on investment).\nA secondary benefit to this exercise is you\u0026rsquo;ve made your request generic. You\u0026rsquo;ve distilled an ideal specific implementation into a problem statement. This opens the feature up to alternative designs that solve the same problem. This problem can now utilize the collective intelligence of the engineers building the feature. If they have experience solving this problem in a better way, they know they can pitch it because they know the problem you want to solve.\nAdding additional context to a story # When planning with your team there may be suggestions from teammates about how to approach the problem or a pro-tip that can clarify where to start. These remarks are not part of the What, Why, or Acceptance Criteria. Where do we put it then?\nI suggest adding a \u0026ldquo;Developer Notes\u0026rdquo; section in a smaller heading size at the bottom of the ticket. In this section, put any suggestions or context that will be helpful to whomever picks up the ticket. After the story is completed, I will often delete this section completely. This section is information for the implementor to take as a suggestion, they either did or they didn\u0026rsquo;t. After the story is delivered whether they followed the advice or not is moot.\nHandling technical debt # I\u0026rsquo;ll keep this section short. As you are working through a queue of work, leave the codebase a little better than you found it.\nStepping over technical debt is just a bad idea.\nStopping everything to tackle technical debt until it\u0026rsquo;s gone is just as bad.\nAs you work on features, if you come into some technical debt, give yourself a timebox to make it 1% better.\nCannot iterate on tech debtIf you are hitting technical debt that you cannot make 1% better, you likely need to spend the time to raise the quality bar on code reviews. Technical Debt that cannot be iteratively improved upon is rare. If every engineer makes the code 1% better each time they touch it, bad decisions or code gets corrected very quickly. What\u0026rsquo;s a bug versus a feature? # Some systems call \u0026ldquo;bugs\u0026rdquo; a \u0026ldquo;defect\u0026rdquo; to reinforce this concept. A defect is something that was intended to have been built in a feature, but was missed. A bug is not acceptance criteria that was forgotten. Forgetting acceptance criteria should be treated as a new feature to add that new functionality.\nIt\u0026rsquo;s not worth going back-and-forth over \u0026ldquo;feature vs bug\u0026rdquo; as the work is the same, but this section was added here for completeness.\nChores: Handling research tasks # I get this question all the time, \u0026ldquo;Oh so Chores are like Spikes.\u0026rdquo;. Wrong.\nA chore is a special kind of story that is intended to drive one or more new feature tickets. As an example lets stick with our login system feature. If I was writing those features and I couldn\u0026rsquo;t write anything but TODO or TBD in various sections, I need to start with a Chore. That Chore would be something like this:\n## What In order to design a login system, we need to research how these systems are commonly built. ## Why Login systems are a common UX pattern that users can be very confused with as-is. If our system does not match our competitors, it will cause more confusion and likely more support requests which costs us a bunch of money. ## Acceptance Criteria - Compare login flows for Acme Co \u0026amp; Beta Inc - Draft a recommendation on which system we should mirror highlighting the reasons. - Break down recommended solution into new feature tickets linked to this one. This ticket can now be worked on. It matches the format of a feature ticket, it has a What, Why, and Acceptance Criteria. Why is it not a feature? Well doing this ticket does not provide measured impact on the business (no ROI, but drives future potential ROI).\nEstimation # Estimation is a tool used by engineers to set delivery expectations, it is not something directly relevant to product. If your team is able to set and hit a delivery date without estimation, you can opt out of doing traditional agile estimates.\nEstimates like Fibonacci and T-Shirt sizing are for engineering only. Sharing \u0026ldquo;Well this will take longer because they are 4 XL tickets\u0026rdquo; with product isn\u0026rsquo;t the correct way to communicate with product. The correct way to communicate this is \u0026ldquo;This will take longer as the complexity with this task isn\u0026rsquo;t something our team is too familiar with.\u0026rdquo; Why change the phrasing? There are a variety of reasons your team may have arrived at XL for a given story. When communicating with product, that should be translated from story-point into an expectation.\nSome common reasons for large story sizing:\nThe team lacks experience in the problem-space, so we\u0026rsquo;re hedging against that risk. External dependencies, such as other teams input, that we cannot derisk any more than we already have. Remember that estimation is a tool that sets a deadline. Give yourself and your team ample time to deliver. Make sure if someone is sick, the team can absorb the inconvenience and still deliver on time.\nNOTE: Giving extra time for unexpected things also releases stress from the team. It keeps them further away from burnout as they know a hiccup isn\u0026rsquo;t going to risk the entire project.\n","externalUrl":null,"permalink":"/handbook/scoping/","section":"Dev Handbook","summary":"How to break a problem into features that are small enough to deliver, clear enough to estimate, and valuable enough to justify the work.","title":"🔭 From Problem to Feature: Scoping Work","type":"handbook"},{"content":"Planning is where the team decides what to work on next. The output is a backlog that\u0026rsquo;s ordered, estimated, and ready to be picked up. If the team leaves planning confused about what they\u0026rsquo;re building or why, the session failed.\nWhen you need this # Engineers frequently ask \u0026ldquo;what should I work on next?\u0026rdquo; because the backlog is unclear or unordered. The team over-commits and consistently carries work over from one iteration to the next. Priorities shift mid-iteration without the team knowing, and people find out their work was deprioritized after they\u0026rsquo;ve already started. Tickets land in \u0026ldquo;In Progress\u0026rdquo; that nobody remembers agreeing to work on. When to run it # Once per iteration, at the start. For most teams that means weekly or biweekly. The cadence matters less than the consistency. Pick a rhythm and protect it.\nWho should be there? # The team lead, the engineers, and ideally someone from product or the stakeholder side. Product\u0026rsquo;s job is to answer \u0026ldquo;why does this matter?\u0026rdquo; and \u0026ldquo;what does success look like?\u0026rdquo; Engineering\u0026rsquo;s job is to answer \u0026ldquo;how complex is this?\u0026rdquo; and \u0026ldquo;what\u0026rsquo;s the approach?\u0026rdquo;\nIf product can\u0026rsquo;t attend, the team lead needs to represent their priorities accurately. Don\u0026rsquo;t guess. If you\u0026rsquo;re not sure what product wants, find out before planning starts.\nKnow who the decision maker is # Planning is a collaborative conversation, not a vote. Everyone should have input, but everyone should also know who makes the final call when there\u0026rsquo;s a disagreement.\nDesigning by committee is one of the fastest ways to grind a planning session to a halt. You\u0026rsquo;ll see it happen: two people have strong but different opinions on priority or approach, and the group goes back and forth for 20 minutes without resolving it. That\u0026rsquo;s a sign that nobody knows who breaks the tie.\nBefore planning starts, make it explicit. Usually it\u0026rsquo;s the team lead for technical decisions and product for priority decisions. The names matter less than the clarity. If the team knows \u0026ldquo;when we can\u0026rsquo;t agree on scope, Sarah decides\u0026rdquo; and \u0026ldquo;when we can\u0026rsquo;t agree on approach, Marcus decides,\u0026rdquo; disagreements resolve in seconds instead of derailing the session.\nThis also improves outcomes. When a single person is accountable for a decision, they feel the weight of it. They listen more carefully, think it through, and own the result. When a decision is made \u0026ldquo;by the group,\u0026rdquo; nobody owns it, which means nobody feels responsible when it turns out to be wrong, and nobody is motivated to course-correct.\nDecision maker, not dictatorThe decision maker\u0026rsquo;s job is to listen, then decide. They should hear the arguments and weigh them seriously. But once the call is made, the team moves forward. Relitigating the same decision across multiple standups is a sign the team doesn\u0026rsquo;t trust the process. The format # 1. Review last iteration # Start by looking at what got done and what didn\u0026rsquo;t. This isn\u0026rsquo;t a blame session. It\u0026rsquo;s a calibration exercise. If the team consistently over-commits, that\u0026rsquo;s a planning problem, not a performance problem.\nQuestions to ask:\nDid we hit the goal we set last time? What carried over, and why? Were there any surprises that threw off the plan? Carryover tickets go back into the backlog. Don\u0026rsquo;t assume they\u0026rsquo;re still the top priority just because they were started. Re-evaluate them alongside everything else.\n2. Set the goal for this iteration # One sentence. What is the team trying to accomplish by the end of this iteration? \u0026ldquo;Ship the login flow\u0026rdquo; is a goal. \u0026ldquo;Work on a bunch of tickets\u0026rdquo; is not.\nThe goal gives the team a filter for every decision they make during the iteration. When someone asks \u0026ldquo;should I pick up this unrelated bug?\u0026rdquo;, the answer is \u0026ldquo;does it help us hit the goal?\u0026rdquo; If not, it can wait.\n3. Walk through the candidates # Pull tickets from the top of the backlog (and scan the icebox for anything newly relevant). For each ticket:\nDoes everyone understand the problem? If not, clarify it now. Don\u0026rsquo;t send people off to build something they don\u0026rsquo;t understand. Are the acceptance criteria clear? An engineer should be able to read the ticket and start working without a follow-up conversation. Is this the right size? If a ticket feels like it\u0026rsquo;ll take more than a few days, it probably needs to be broken down. (See From Problem to Feature: Scoping Work for guidance on this.) 4. Estimate # If your team uses estimation, this is where it happens. See the Estimating page for the mechanics of how to run a pointing session.\nNot every team needs formal estimates. If your team can reliably commit to a set of work and deliver it, you can skip this step entirely. Estimation is a tool for setting expectations, not a ceremony you owe to the agile gods.\n5. Commit # The team agrees on the set of tickets for this iteration. This is a commitment, not a wish list. If you\u0026rsquo;re not confident the team can finish everything, cut scope now. It\u0026rsquo;s much better to finish 5 tickets than to start 8 and finish 4.\nThe planning smell testAfter planning, every engineer should be able to answer two questions without looking anything up: \u0026ldquo;What is the goal this iteration?\u0026rdquo; and \u0026ldquo;What am I picking up first?\u0026rdquo; If they can\u0026rsquo;t, the session didn\u0026rsquo;t land. Common problems # Planning takes forever. If your planning sessions run over an hour for a small team, you\u0026rsquo;re probably refining tickets during planning instead of before it. Tickets should arrive at planning mostly ready. Use a separate backlog refinement session (even async) to get them into shape ahead of time.\nThe team over-commits every iteration. This is the most common planning failure. People are optimistic. They forget about meetings, code reviews, production incidents, and the fact that someone is taking Friday off. Build in slack. If the team has 8 days of capacity, plan for 6 days of work.\nNobody pushes back on priorities. If the team accepts every ticket without discussion, they\u0026rsquo;re either not engaged or they don\u0026rsquo;t feel like they have permission to challenge scope. The team lead should actively invite pushback: \u0026ldquo;Is this really more important than X?\u0026rdquo; or \u0026ldquo;Are we sure we need all of these acceptance criteria?\u0026rdquo;\nPlanning without product inputIf the team is planning work without any input from product or stakeholders, you\u0026rsquo;re building in a vacuum. It might feel faster, but you\u0026rsquo;ll pay for it later when you deliver something that doesn\u0026rsquo;t match expectations. Even a 5-minute async check-in before planning is better than nothing. ","externalUrl":null,"permalink":"/handbook/rituals/planning/","section":"Dev Handbook","summary":"How to run a planning session that produces a clear, committed backlog with known decision makers.","title":"🗺️ Planning: Deciding What to Build Next","type":"handbook"},{"content":"Every practice in this handbook (scoping, the board, rituals, templates) only works if someone is paying attention to whether the system is actually running. That someone is the project lead. This page is the daily checklist for making sure the gears are turning and nothing is quietly falling apart.\nIf you\u0026rsquo;re not a project lead, this page is still useful. It tells you what a healthy project looks like from the outside, so you can spot when things are drifting.\nStart of the day # Before standup, take five minutes to scan the board:\nAssignments are correct. Everyone is assigned to exactly one ticket. Nobody is assigned to something they\u0026rsquo;ve moved off of. If someone is out sick, unassign their ticket. (Encourage people to do this themselves, but verify.) PRs link to tickets. Every open pull request should reference its issue. If it doesn\u0026rsquo;t, the board and the code are telling two different stories. \u0026ldquo;Done\u0026rdquo; tickets are actually done. Glance at anything recently moved to Done. Can you stop thinking about it entirely? If not, it\u0026rsquo;s not done. (See Your Project Board is a Mirror for more on this.) Blocked tickets aren\u0026rsquo;t being ignored. If something has been blocked for more than a day with no visible activity toward unblocking it, ping the channel and ask why. This gets faster over timeThe first few weeks, this morning scan might take 10-15 minutes. Once the team internalizes these habits, most of it is already done before you look. You\u0026rsquo;re just verifying, not fixing. Keeping tickets up to date # When all the deliverables are complete, you should be able to close out the ticket and never think about it again. If you feel like things are missing on a ticket that\u0026rsquo;s in progress, add them or reach out to the person working on it. Be conscious when closing a ticket out: are there small details that still need to happen? Flipping beta flags, verifying on prod, updating documentation. Add those things to the acceptance criteria now, not after someone forgets.\nMultiple pull requests per ticket are fine. If you have multiple PRs for a single ticket, link each PR next to its related deliverable so reviewers can trace the work.\nHandling blocked tickets # Tickets get blocked on decisions from design/product, pull requests in other repos, or waiting for testing to complete. When a ticket gets blocked:\nAdd the \u0026ldquo;blocked\u0026rdquo; label. Add a # Blocked section at the top of the ticket explaining why it\u0026rsquo;s blocked and what needs to happen to unblock it. If you can, tag the person who can unblock it. Encourage the team to do this themselves. If you notice blocked tickets without an explanation, ask for one. A blocked ticket with no context is invisible to everyone except the person who blocked it.\nReassigning tickets # If a ticket gets blocked or someone stops working on it to help with something else, encourage them to move it to the top of the backlog and unassign themselves. This does two things: it signals to the team that nobody is working on it, and it makes it the next thing someone picks up.\nEncourage the team to always scan blocked tickets in the backlog and check if they can now be unblocked. It\u0026rsquo;s easy to gloss over them. A ticket that was blocked last week might be unblockable today because the dependency shipped, but nobody noticed.\nLimit work in progress # No one should be assigned to more than one ticket \u0026ldquo;In Progress\u0026rdquo; at a time. If your team encourages pairing, the maximum number of in-progress tickets should be roughly team size / 2. On a team of 4, follow up if you see more than 2 tickets in progress. (See Your Project Board is a Mirror for why this matters.)\nMaintain a true definition of \u0026ldquo;done\u0026rdquo; # A great test: ask yourself \u0026ldquo;Can we stop thinking about this entirely when this ticket is marked Done?\u0026rdquo;\nIf you hear the words \u0026ldquo;Yeah it\u0026rsquo;s done, but\u0026hellip;\u0026rdquo; then deliverables are missing. \u0026ldquo;Yeah it\u0026rsquo;s done, but we haven\u0026rsquo;t tested on prod.\u0026rdquo; \u0026ldquo;Yeah it\u0026rsquo;s done, but we\u0026rsquo;re waiting on data before we can test.\u0026rdquo; If you can\u0026rsquo;t close out the ticket until something else happens, block the ticket and explain what\u0026rsquo;s needed.\nClosing out tickets # Once a story is approved and moved to Done, take a few minutes to clean it up. Remove anything no longer relevant. Add screenshots of the finished work. Link pull requests next to their acceptance criteria items. Mark each deliverable with a ✅.\nThis turns the ticket into long-lived documentation. Six months from now, when someone asks \u0026ldquo;how does this feature work?\u0026rdquo; or \u0026ldquo;when did we ship that?\u0026rdquo;, a well-closed ticket is the answer.\nBefore Done # ## What Provide a way for users to switch accounts. ## Why Most of our customers manage multiple accounts. Without a way for our customers to switch between multiple accounts, we are losing customers to our competition which supports this feature. ## Acceptance Criteria - When I am logged in, I can see a logout button in the navigation bar. - After clicking the logout button, I\u0026#39;m redirected to the login page where I can log in as another user. - Entering the credentials for my user, I should see my new username on the account page. ### Developer Notes - Check out the internal service where they implemented something similar. - Please favor using an existing implementation over adding a new library to the codebase. - The login button can be any color, but we all prefer for it to be red like other destroy actions. After Done # Remove sections no longer relevant. Add screenshots so everyone can see what was built. Link pull requests for engineers who want to go deeper. Delete the Developer Notes section; whether the engineer followed the advice is irrelevant once the work is done.\nIf things were taken out of scope, just remove them. What you didn\u0026rsquo;t do won\u0026rsquo;t matter to your future self. What you did, what it looked like, where it happened, and why you did it will matter.\n## What Provide a way for users to switch accounts. ## Why Most of our customers manage multiple accounts. Without a way for our customers to switch between multiple accounts, we are losing customers to our competition which supports this feature. ## Acceptance Criteria - ✅ When I am logged in, I can see a logout button in the navigation bar. (see pull request #1234) - ✅ After clicking the logout button, I\u0026#39;m redirected to the login page where I can log in as another user. (see pull request #1234) - ✅ Entering the credentials for my user, I should see my new username on the account page. (see pull request #1332) ## Screenshots ### Logged Out Page ![loggedout.jpg](…) ### Logged In Page ![loggedin.jpg](…) ","externalUrl":null,"permalink":"/handbook/daily-routine/","section":"Dev Handbook","summary":"The project lead’s daily checklist for making sure the board reflects reality and nothing is quietly falling apart.","title":"☀️ The Daily Routine: Keeping It All Running","type":"handbook"},{"content":"A feature proposal is how an idea gets evaluated before it becomes work. The most important thing it does is force the author to articulate the problem before jumping to a solution. \u0026ldquo;We should add dark mode\u0026rdquo; is a solution. \u0026ldquo;Our users report eye strain during long sessions\u0026rdquo; is a problem. The proposal starts with the problem.\nWhen you need this # Features get built because someone thought they were cool, not because they solve a real user problem. The team has no structured way to evaluate whether a feature is worth building before committing engineering time. Stakeholders propose solutions directly (\u0026ldquo;add a button that does X\u0026rdquo;) without explaining the underlying need, leaving no room for the team to suggest better alternatives. What each section is for # Problem to solve - The core of the proposal. What pain does the user have today? Be specific. \u0026ldquo;Users can\u0026rsquo;t do X\u0026rdquo; is better than \u0026ldquo;improve the user experience.\u0026rdquo; If you can\u0026rsquo;t clearly state the problem, the feature isn\u0026rsquo;t ready to propose.\nIntended users - Who benefits from this? A specific user type, persona, or role. \u0026ldquo;Everyone\u0026rdquo; is almost never the right answer. Knowing the audience shapes every design decision that follows.\nFurther details - Use cases, benefits, and how this connects to the bigger picture. This is where you make the case for why this problem is worth solving now instead of later.\nProposal - Your suggested solution. This should include the user journey: what does the user do, what do they see, how does their workflow change? Keep in mind that this is a starting point, not a final design. The team may come up with a better approach. (See From Problem to Feature: Scoping Work for more on keeping proposals implementation-independent.)\nDocumentation - How would you explain this feature to a user? If you can\u0026rsquo;t describe it simply, the feature might be too complex. This section also serves as a reminder that documentation is part of shipping, not an afterthought.\nTesting - What risks does this introduce? What could break? This forces the proposer to think about the cost of the feature, not just the benefit.\nSuccess metrics - How will you know this worked? Define both the business outcome (\u0026ldquo;reduce support tickets by 20%\u0026rdquo;) and the acceptance criteria (\u0026ldquo;user can do X without error\u0026rdquo;). If there\u0026rsquo;s no way to measure success, that\u0026rsquo;s worth flagging now rather than after you\u0026rsquo;ve built it.\nThe template # ### Problem to solve \u0026gt; What problem do we solve? ### Intended users \u0026gt; Who will use this feature? If known, include any of the following: types of \u0026gt; users (e.g. Developer), personas, or specific company roles (e.g. Release \u0026gt; Manager). It\u0026#39;s okay to write \u0026#34;Unknown\u0026#34; and fill this field in later. ### Further details \u0026gt; Include use cases, benefits, and/or goals (contributes to our vision?) ### Proposal \u0026gt; How are we going to solve the problem? Try to include the user journey! ### Documentation \u0026gt; How would you document or explain this new feature in our documentation ### Testing \u0026gt; What risks does this change pose? How might it affect the quality of the \u0026gt; product? What additional test coverage or changes to tests will be needed? \u0026gt; Will it require cross-browser testing? ### What does success look like, and how can we measure that? \u0026gt; Define both the success metrics and acceptance criteria. Note that success \u0026gt; metrics indicate the desired business outcomes, while acceptance criteria \u0026gt; indicate when the solution is working correctly. If there is no way to measure \u0026gt; success, link to an issue that will implement a way to measure this. ### Links / references ","externalUrl":null,"permalink":"/handbook/templates/feature-proposal/","section":"Dev Handbook","summary":"Force yourself to articulate the problem before jumping to a solution. The proposal starts with the problem.","title":"💡 Feature Proposals: Start With the Problem","type":"handbook"},{"content":"The retrospective is the team\u0026rsquo;s primary mechanism for self-improvement. If you could only keep one ritual, this would be the one. Everything else helps the team execute. The retro helps the team get better at executing.\nThe format is simple: look back at the last iteration, talk about what\u0026rsquo;s working and what isn\u0026rsquo;t, and agree on one or two small changes to try next time. That\u0026rsquo;s it. The value isn\u0026rsquo;t in the conversation itself. It\u0026rsquo;s in what changes as a result of the conversation.\nWhen you need this # The same problems keep happening and nobody is sure why or how to fix them. The team is frustrated but there\u0026rsquo;s no structured place to talk about it. Process changes happen top-down and the people doing the work don\u0026rsquo;t have a voice in shaping how they work. Things went well, but the team can\u0026rsquo;t articulate why, which means they can\u0026rsquo;t repeat it intentionally. Who should be there? # The team. Engineers, team lead, and optionally product. Rotate the facilitator each session so nobody falls into the habit of \u0026ldquo;running\u0026rdquo; the retro. The facilitator\u0026rsquo;s job is to keep time, make sure everyone speaks, and push the group toward action items. They\u0026rsquo;re not in charge of the outcomes.\nThe format # 1. What went well? # Start positive. What worked this iteration? Did a process change from last retro actually help? Did pairing on a tricky ticket go better than expected? Did the team hit its goal?\nThis isn\u0026rsquo;t filler to make people feel good. It\u0026rsquo;s how the team identifies what to keep doing. If nobody can name anything that went well, that\u0026rsquo;s a signal worth paying attention to.\n2. What didn\u0026rsquo;t go well? # This is where the retro earns its keep. What slowed the team down? Where did things get confusing? What was frustrating?\nA few ground rules:\nTalk about the process, not the people. \u0026ldquo;Code reviews took too long this iteration\u0026rdquo; is productive. \u0026ldquo;Alex takes forever to review PRs\u0026rdquo; is not. The retro is a safe space for honest feedback about how the team works together. The moment it becomes a place where people get called out, people stop being honest.\nBe specific. \u0026ldquo;Communication could be better\u0026rdquo; is too vague to act on. \u0026ldquo;I didn\u0026rsquo;t know the API contract changed until my tests broke\u0026rdquo; is something the team can actually fix.\nThe facilitator should watch for silence. If the same two people are doing all the talking, directly invite others in. \u0026ldquo;Anything you\u0026rsquo;d add?\u0026rdquo; goes a long way.\n3. Action items # This is the only part that matters.\nNo action items = no retroA retrospective without clear action items is just a venting session. Venting might feel cathartic in the moment, but over time it becomes demoralizing. The team raises the same frustrations week after week, nothing changes, and eventually people stop bringing things up because they\u0026rsquo;ve learned it doesn\u0026rsquo;t lead anywhere.\nEvery retro should produce at least one concrete action item with a specific person responsible for it. Not \u0026ldquo;the team will try to communicate better.\u0026rdquo; Instead: \u0026ldquo;Jamie will post API changes in the #eng channel before merging. We\u0026rsquo;ll check next retro if this helped.\u0026rdquo;\nWhat makes a good action item? # Narrow and incremental. The goal is a small adjustment you can try for one iteration and evaluate. Not a process overhaul, not a new tool adoption, not \u0026ldquo;we should rethink how we do code reviews.\u0026rdquo; Try \u0026ldquo;we\u0026rsquo;ll add a 24-hour SLA for PR reviews this iteration and see if it helps.\u0026rdquo;\nOwned by a person. \u0026ldquo;We should do X\u0026rdquo; means nobody does X. \u0026ldquo;Sarah will do X\u0026rdquo; means it gets done, and the team can check in on it next retro.\nReversible. The best action items are experiments, not permanent policy changes. If it doesn\u0026rsquo;t work, you drop it. This makes it safe to try things without the pressure of getting it right the first time.\nIterate, don\u0026#39;t oscillateResist the temptation to make radical changes based on a single bad iteration. If code reviews were slow this week, the answer is probably a small tweak (set a review SLA) not a dramatic swing (skip code reviews entirely). Jumping between extremes based on one retro creates whiplash. The team never settles into a rhythm because the process keeps changing underneath them. Small adjustments, consistently applied, compound into big improvements over time. Checking last iteration\u0026rsquo;s action items # Every retro should start by revisiting the action items from last time. Did we do them? Did they help?\nThis is the accountability loop that makes retros actually work. If the team committed to trying something and nobody followed through, that\u0026rsquo;s worth discussing. If they followed through and it didn\u0026rsquo;t help, great, drop it and try something else. Either way, closing the loop proves that the retro leads to real change, which is what keeps people engaged.\nCommon problems # The same issue comes up every retro. Either the action items aren\u0026rsquo;t specific enough, nobody is owning them, or the team is working around a structural problem that small tweaks can\u0026rsquo;t fix. If something has surfaced three retros in a row, it needs a bigger conversation outside the retro.\nPeople don\u0026rsquo;t feel safe being honest. This usually means the retro has become a performance review in disguise, or that past feedback led to someone getting blamed. The facilitator needs to actively reinforce that the retro is about the process, not individuals. If trust is really broken, consider anonymous input (sticky notes, a shared doc before the meeting) as a bridge until the team rebuilds safety.\nThe retro runs too long. 30 to 45 minutes is plenty for a small team. If it\u0026rsquo;s going longer, the facilitator is letting discussions spiral. Timebox each section. If a topic needs more time, take it offline and give it a dedicated conversation.\nAction items are too ambitious. If the team leaves the retro with five action items and a process redesign, none of it will happen. One or two small changes per iteration. That\u0026rsquo;s enough. Trust the compounding.\n","externalUrl":null,"permalink":"/handbook/rituals/retrospectives/","section":"Dev Handbook","summary":"The team’s primary self-improvement mechanism. Useless without action items; powerful with them.","title":"🪞 Retrospectives: How the Team Gets Better","type":"handbook"},{"content":" When you need this # The team consistently misses delivery expectations and nobody can explain why. Stakeholders are asking \u0026ldquo;when will this be done?\u0026rdquo; and the team has no framework for answering. Some tickets take dramatically longer than expected, and there\u0026rsquo;s no conversation happening beforehand about complexity or risk. The team is new to working together and doesn\u0026rsquo;t yet have a shared sense of what \u0026ldquo;this is a big one\u0026rdquo; means. If your team is reliably delivering on time without formal estimation, you may not need this. Estimation is a calibration tool, not a requirement.\nWhat estimation is (and isn\u0026rsquo;t) # Estimation measures risk, not hours. A \u0026ldquo;3\u0026rdquo; doesn\u0026rsquo;t mean \u0026ldquo;three days of work.\u0026rdquo; It means \u0026ldquo;this is unfamiliar, there are unknowns, and if our first approach doesn\u0026rsquo;t pan out we could be back to the drawing board.\u0026rdquo; The number captures how confident the team is, not how long they think it\u0026rsquo;ll take.\nThis distinction matters because time-based estimates invite micromanagement. If you say \u0026ldquo;this will take 3 days\u0026rdquo; and it takes 5, someone wants to know what went wrong. If you say \u0026ldquo;this is high risk\u0026rdquo; and it takes longer than expected, the team already agreed that was possible. The conversation shifts from blame to learning.\nFor the philosophy of how estimation connects to delivery expectations and how to communicate estimates to product, see the Estimation section in From Problem to Feature.\nThe pointing scale # The point estimate for a story is between 0 and 3. These values assess the risk involved, with 0 being the absolute lowest and 3 being the highest.\n0 - Copy changes, absolute no-brainer items. 1 - Straight-forward change. The approach is known and comfortable to the developer. 2 - Straight-forward with a small set of approaches available. If the first option doesn\u0026rsquo;t work out, another has to be tried. 3 - Unfamiliar and risky. If an idea doesn\u0026rsquo;t pan out, it could be back to the drawing board to figure out other options. Why 0-3 instead of Fibonacci or t-shirt sizing? # A small scale forces precision. With Fibonacci (1, 2, 3, 5, 8, 13), teams tend to cluster everything in the 3-5 range and the higher numbers become meaningless. T-shirt sizing (S, M, L, XL) is even vaguer. A 0-3 scale gives you just four options, which means every number has a distinct meaning and disagreements are easier to resolve.\nAvoid using 0As a general rule, avoid estimates of 0 except in the most trivial cases. If a story is truly a 0, it\u0026rsquo;s probably too granular to be its own ticket. Frequent 0s also distort team velocity, making it look like the team is completing more work than they actually are. How to run a pointing session # 1. Read the ticket aloud # The team lead or facilitator reads the ticket. Everyone should understand the problem, the acceptance criteria, and any relevant context before pointing. If someone has a question, answer it now. Pointing a ticket nobody understands is a waste of time.\n2. Everyone points at once # This is critical. On a count of three, everyone holds up their number simultaneously. If people point sequentially, the first person\u0026rsquo;s estimate anchors everyone else. You want independent assessments, not groupthink.\nFor remote teams, use a tool that hides votes until everyone has submitted, or simply count down and have everyone type their number in chat at the same time.\n3. Discuss the outliers # If everyone points a 1 and one person points a 3, don\u0026rsquo;t just average it. Ask the 3 why. They may know something the rest of the team doesn\u0026rsquo;t (a hidden dependency, a past experience with a similar problem, a piece of the codebase that\u0026rsquo;s harder to change than it looks). Equally, ask the 1s why they\u0026rsquo;re confident. The conversation is where the real value is, not the number.\n4. Re-point if needed # After discussion, if the team\u0026rsquo;s understanding has changed, point again. Usually one round of discussion is enough to converge. If the team still can\u0026rsquo;t agree after two rounds, go with the higher number. Optimism is the enemy of accurate estimation.\nPoint the ticket, not the personA common mistake is estimating based on who you think will pick up the ticket. \u0026ldquo;Well if Sarah does it, it\u0026rsquo;s a 1, but if I do it, it\u0026rsquo;s a 3.\u0026rdquo; Always estimate as if an average team member is doing the work. If you point based on the best person for the job, your estimates only hold if that specific person is available, which they won\u0026rsquo;t always be. Common problems # Everything is a 2. If the team is pointing 2 on every ticket, the scale has lost its meaning. This usually means tickets are all roughly the same size (which could mean they\u0026rsquo;re well-scoped) or the team isn\u0026rsquo;t thinking critically about risk. Push back occasionally: \u0026ldquo;What would make this a 3? What would make this a 1?\u0026rdquo;\nEstimates don\u0026rsquo;t match reality. If tickets estimated as 1 consistently take as long as tickets estimated as 3, the team\u0026rsquo;s calibration is off. Use retros to discuss which estimates were accurate and which weren\u0026rsquo;t. Over time, the team develops a shared sense of what each number means.\nPeople treat estimates as commitments. An estimate is a confidence signal, not a deadline. If a 2 takes longer than expected, that\u0026rsquo;s useful information for next time, not a failure. The moment estimates become commitments, people start inflating them defensively and the whole system loses value.\nThe pointing session takes too long. If you\u0026rsquo;re spending 10 minutes on a single ticket, the ticket probably isn\u0026rsquo;t ready to be estimated. It needs more refinement. A well-written ticket with clear acceptance criteria should take under 2 minutes to point.\n","externalUrl":null,"permalink":"/handbook/rituals/estimating/","section":"Dev Handbook","summary":"A simple 0-3 pointing scale, how to run a session, and why you’re measuring confidence not hours.","title":"⚖️ Estimating: Measuring Risk, Not Time","type":"handbook"},{"content":"The story is the fundamental unit of work on the project board. It describes a small, deliverable piece of value: what the team is building, why it matters, and how to verify it\u0026rsquo;s done. If your stories are well-written, everything downstream (estimation, standups, code review, QA) gets easier. If they\u0026rsquo;re vague, every step of the process will fight you.\nFor the philosophy behind how to scope stories, see From Problem to Feature: Scoping Work. This page focuses on the practical template.\nWhen you need this # Engineers start working on a ticket and immediately need a 20-minute conversation to understand what\u0026rsquo;s being asked. Acceptance criteria are missing or vague, leading to \u0026ldquo;is this done?\u0026rdquo; debates during review. Tickets describe implementation tasks (\u0026ldquo;refactor the auth module\u0026rdquo;) instead of user-facing value (\u0026ldquo;users can switch accounts\u0026rdquo;). Work gets marked as \u0026ldquo;done\u0026rdquo; but loose ends keep surfacing because nobody captured the full definition of done. What each section is for # Background - A couple of sentences of context. What does the reader need to know to understand this request? If the ticket uses domain-specific terms, define them here. Don\u0026rsquo;t assume the person picking up the ticket was in the meeting where this was discussed.\nDesired Behavior - What should the user experience look like when this is done? Describe it from the user\u0026rsquo;s perspective: \u0026ldquo;When I go to X, I see Y.\u0026rdquo; Words are more important than screenshots here. Screenshots go stale as the product changes; a clear written description stays accurate. If a screenshot truly helps, include it as a supplement, not a replacement.\nBusiness Case / Why? / ROI - Why is this worth doing right now instead of something else? This helps the team prioritize and challenge scope. If you\u0026rsquo;re not sure why this is important, it\u0026rsquo;s better to remove this section than to exaggerate. An honest \u0026ldquo;this is low priority but easy to knock out\u0026rdquo; is more useful than a manufactured justification.\nAcceptance Criteria - The checklist for \u0026ldquo;done.\u0026rdquo; Each item should be verifiable by someone who wasn\u0026rsquo;t involved in the implementation. Write them as checkboxes so they can be ticked off during QA.\nThis list should include non-engineering tasks too: notifying a customer, updating documentation, flipping a feature flag. If it needs to happen before the ticket can be closed, it belongs here. (See Your Project Board is a Mirror for more on the definition of done.)\nUse checkboxes, not bulletsAcceptance criteria written as a bullet list look the same whether they\u0026rsquo;ve been verified or not. Checkboxes create accountability. Each item gets explicitly checked off, so it\u0026rsquo;s obvious at a glance what\u0026rsquo;s been verified and what hasn\u0026rsquo;t. When a ticket is \u0026ldquo;done,\u0026rdquo; every box should be checked. Developer Notes - Optional context from the team to help whoever picks up the ticket. Things like \u0026ldquo;there\u0026rsquo;s a similar implementation in the billing service\u0026rdquo; or \u0026ldquo;the design team prefers we use the existing component library.\u0026rdquo; This section is a suggestion, not a requirement. After the story is delivered, delete it. Whether the engineer followed the advice or not is irrelevant once the work is done.\nDeveloper Notes are disposableDon\u0026rsquo;t treat Developer Notes as part of the story\u0026rsquo;s permanent record. They\u0026rsquo;re scaffolding. When the ticket is closed, remove them and clean up the ticket so it reads as documentation of what was built, not a record of how the sausage was made. The template # ## Background \u0026lt;!-- Explain any background or assumed knowledge to understand this story. A couple sentences usually. This is also a good section to clarify the usage of terms that others may not know but will be used to describe this request. --\u0026gt; ## Desired Behavior \u0026lt;!-- Explain what you would like to see. When I go to X, I want to see A, B, and not C. Words are more important here than a screenshot. Screenshots can often lead to more questions than they are worth because they are too broad (more context than is needed) or outdated from the site. This means that in a few weeks time, they no longer make sense as the app has changed. --\u0026gt; ## Business Case / Why? / ROI \u0026lt;!-- Why are we doing this over another task? If unsure, it is better to remove this section than exaggerate a request\u0026#39;s importance. This will help the team challenge if this story is more or less important to prioritize against others being considered. --\u0026gt; ## Acceptance Criteria \u0026lt;!-- Call out what you expect to check when it comes time to QA this feature. This list should also include things non-development related. Often this can be notifying the requesting customer of the fix or updating documentation. If something is missing in QA, it will often result then in a new feature request. Using a check-list here will make sure things have been verified versus getting missed when it\u0026#39;s a bullet-list. --\u0026gt; - [ ] When adding A, B, and C. The admin can see all of these. - [ ] When adding A, B, and C. The viewer can only see C as it has been approved. - [ ] Notify {customer} that the fix was shipped. - [ ] Update Wiki relating to this feature. - [ ] Update documentation ## Developer Notes \u0026lt;!-- To be filled in by developers to help accelerate or clarify the above. Things like where to find code to copy or start with, or translation of terms between the above. --\u0026gt; ","externalUrl":null,"permalink":"/handbook/templates/story/","section":"Dev Handbook","summary":"The fundamental unit of work on the board. What the team is building, why it matters, and how to verify it’s done.","title":"📋 Stories: The Building Block of Delivery","type":"handbook"},{"content":"If someone walked up to your team and asked \u0026ldquo;What are you all working on right now?\u0026rdquo;, the project board should answer that question in under 30 seconds. No meetings, no Slack threads, no \u0026ldquo;let me pull up my notes.\u0026rdquo;\nThe board is the single source of truth for the project. It tells the team and its stakeholders four things:\nWhat is next? What\u0026rsquo;s done? Who\u0026rsquo;s working on what? How much do we have left? When the board is healthy, most \u0026ldquo;sync\u0026rdquo; conversations become unnecessary. When it\u0026rsquo;s not, you\u0026rsquo;ll notice the symptoms quickly: people asking for status updates, engineers unsure what to pick up next, and stakeholders who feel out of the loop no matter how many meetings you schedule.\nBacklog: What is next? # The backlog is a single, prioritized list. One list. Not a list per engineer, not a list per feature area, not a \u0026ldquo;frontend backlog\u0026rdquo; and a \u0026ldquo;backend backlog.\u0026rdquo; One list, ordered top to bottom by priority.\nThis sounds simple, but most teams don\u0026rsquo;t actually do it. They end up with tickets scattered across multiple views, labels acting as pseudo-backlogs, or a backlog so long that nobody reads past the first five items.\nA single prioritized backlog solves several problems at once:\nAlignment happens automatically. If the stakeholder\u0026rsquo;s top priority is at the top of the list, the team is working on it. No alignment meeting needed. Priority changes are cheap. Stakeholder changes their mind? Move the ticket up or down. The team sees the change next time they look at the board. \u0026ldquo;What should I work on next?\u0026rdquo; answers itself. Grab the top unassigned ticket. Done. The team focuses together. Instead of five engineers working on five unrelated things, you get natural collaboration on the same goal. People pair up, unblock each other, and finish work faster. Standups get shorter. When everyone can see the board, you don\u0026rsquo;t need to narrate your status. The standup becomes about blockers and adjustments, not recaps. What makes a bad backlog? # If your backlog has more than two weeks of work in it, it\u0026rsquo;s too long. Nobody is reading ticket #47. Anything beyond the near-term horizon belongs in the icebox (more on that below), not cluttering the backlog.\nIf tickets in your backlog don\u0026rsquo;t have clear acceptance criteria, they\u0026rsquo;re not ready. An engineer should be able to pick up the top ticket and start working without needing a 20-minute conversation first. If they can\u0026rsquo;t, the ticket needs more refinement before it earns a spot in the backlog.\nIn Progress: Who\u0026rsquo;s working on what? # Each person should be assigned to exactly one ticket at a time. Not two. Not \u0026ldquo;one main thing and a small side thing.\u0026rdquo; One.\nThis feels restrictive, but it solves a problem that every team hits eventually: context switching. When someone is juggling three tickets, none of them are getting their full attention. Progress on all three slows down, and the board looks like everything is \u0026ldquo;in progress\u0026rdquo; but nothing is actually moving.\nThe WIP limit rule of thumbIf your team encourages pairing, the maximum number of tickets in progress should be roughly team size / 2. On a team of 4, if you see more than 2 tickets in the \u0026ldquo;In Progress\u0026rdquo; column, something is off. Either someone is multitasking, or people aren\u0026rsquo;t pairing up. When someone gets blocked on their ticket, they shouldn\u0026rsquo;t just pick up a second one and leave the first sitting in limbo. They should move the blocked ticket back to the top of the backlog, unassign themselves, and add a # Blocked section at the top of the ticket explaining why it\u0026rsquo;s stuck. Then they grab the next thing.\nThis keeps the board honest. If a ticket is in \u0026ldquo;In Progress,\u0026rdquo; someone is actively working on it right now. If it\u0026rsquo;s blocked, it\u0026rsquo;s visible in the backlog with an explanation, not hiding in a column where everyone assumes someone is on it.\nDone: What\u0026rsquo;s done? # \u0026ldquo;Done\u0026rdquo; is the most abused column on any project board.\nThe test is simple: when a ticket is in \u0026ldquo;Done,\u0026rdquo; can the team stop thinking about it entirely? If the answer is \u0026ldquo;well, yes, but we still need to test on prod\u0026rdquo; or \u0026ldquo;yes, but we haven\u0026rsquo;t told the customer yet,\u0026rdquo; then it\u0026rsquo;s not done. It\u0026rsquo;s almost done, which is a very different thing.\n\u0026#34;Yeah it\u0026#39;s done, but...\u0026#34;If you hear these words, the ticket is missing deliverables. Things like verifying on production, flipping feature flags, notifying stakeholders, or updating documentation are all part of \u0026ldquo;done.\u0026rdquo; If they\u0026rsquo;re not captured in the acceptance criteria, add them now. A ticket that\u0026rsquo;s \u0026ldquo;done but\u0026rdquo; will sit in a grey zone where nobody owns it and the loose ends quietly get forgotten. When a ticket is truly done, take a few minutes to clean it up. Remove the Developer Notes section (it served its purpose). Add screenshots of the finished work. Link the pull requests next to their related acceptance criteria items. Mark each deliverable with a ✅.\nWhy bother? Because six months from now, someone will ask \u0026ldquo;how does this feature work?\u0026rdquo; or \u0026ldquo;when did we ship that?\u0026rdquo; A well-closed ticket becomes living documentation. A sloppy one becomes a dead link that nobody trusts.\nIcebox: How much do we have left? # The icebox is where future ideas live. It\u0026rsquo;s everything the team might work on eventually, but not right now. Feature requests from stakeholders, technical debt someone noticed, ideas that came up during retro, that \u0026ldquo;we should really fix this someday\u0026rdquo; conversation from last month.\nThe key difference between the icebox and the backlog: the backlog is a commitment, the icebox is a parking lot.\nTickets in the backlog are refined, prioritized, and ready to be picked up. Tickets in the icebox might be half-baked, might be missing acceptance criteria, might be a single sentence. That\u0026rsquo;s fine. The icebox is not a place for polished work. It\u0026rsquo;s a place to capture ideas so they don\u0026rsquo;t get lost.\nKeeping the icebox useful # The icebox gets messy fast. Without occasional grooming, it turns into a graveyard of tickets nobody will ever look at again. A few practices that help:\nReview it during planning. When deciding what goes into the backlog next, scan the icebox. You may find something that\u0026rsquo;s suddenly relevant. Delete aggressively. If a ticket has been in the icebox for months and nobody has mentioned it, it\u0026rsquo;s probably not important. Delete it. If it matters, it\u0026rsquo;ll come back up. Don\u0026rsquo;t use it as a guilt pile. The icebox is not a list of things you \u0026ldquo;should\u0026rdquo; be doing. It\u0026rsquo;s a list of options. If having 200 tickets in the icebox stresses the team out, trim it down to the 20 that actually have a chance of getting worked on. The icebox answers the question \u0026ldquo;how much is left?\u0026rdquo; for stakeholders. But be careful with how you frame it. A full icebox doesn\u0026rsquo;t mean the project is behind. It means the team has more ideas than capacity, which is healthy. The backlog is what tells you how much committed work remains.\n","externalUrl":null,"permalink":"/handbook/project-board/","section":"Dev Handbook","summary":"The board should answer ‘what are you working on?’ in 30 seconds. Here’s how to set it up and keep it honest.","title":"📜 Your Project Board is a Mirror","type":"handbook"},{"content":"Every team has meetings. Not every team has rituals.\nThe difference is intent. A meeting is a block on the calendar. A ritual has a specific purpose, a consistent format, and a clear signal for when it\u0026rsquo;s working (or not).\nIn this section:\n🧍 Standups: Daily Alignment, Not Status Reports 🗺️ Planning: Deciding What to Build Next 🪞 Retrospectives: How the Team Gets Better ⚖️ Estimating: Measuring Risk, Not Time Only adopt a ritual if you know why you need it # Rituals should exist to solve a specific problem. \u0026ldquo;We do standups because that\u0026rsquo;s what agile teams do\u0026rdquo; is not a reason. \u0026ldquo;We do standups because people kept working on the wrong things and we needed a daily alignment check\u0026rdquo; is a reason.\nBefore adding any ritual, ask: what problem does this solve? If you can\u0026rsquo;t name it, you don\u0026rsquo;t need the ritual yet. You might never need it. A two-person team shipping a prototype doesn\u0026rsquo;t need iteration planning. A team that\u0026rsquo;s never missed a deadline doesn\u0026rsquo;t need formal estimation. That\u0026rsquo;s fine.\nRituals can be major time wastersA bad ritual is worse than no ritual. A 30-minute standup that nobody finds useful costs your team 2.5 hours per week. Multiply that by a few more ceremonies and you\u0026rsquo;ve burned an entire day of engineering time on meetings that exist out of habit, not necessity. If people are dreading a ritual, or if nothing changes as a result of holding it, that\u0026rsquo;s your signal to fix it or kill it. If a ritual stops being useful, change it or drop it. Keeping one out of habit is worse than not having one at all, because it teaches the team that their time doesn\u0026rsquo;t matter.\nThe rituals in this section are the ones I\u0026rsquo;ve found essential for remote teams. You don\u0026rsquo;t need all of them, and you definitely don\u0026rsquo;t need to run them exactly the way I describe. But if your team is struggling with alignment, delivery, or morale, chances are one of these rituals is either missing or broken.\nEach ritual page covers three things: what the ritual is for, how to run it, and what it looks like when it\u0026rsquo;s going wrong.\n","externalUrl":null,"permalink":"/handbook/rituals/","section":"Dev Handbook","summary":"Recurring ceremonies that keep a project healthy. Only adopt one if you know why you need it.","title":"🥁 Rituals: Not Every Team Needs Every Meeting","type":"handbook"},{"content":"Templates exist so your team doesn\u0026rsquo;t reinvent the wheel every time they file a bug, propose a feature, or open a pull request. A good template isn\u0026rsquo;t bureaucracy. It\u0026rsquo;s a checklist that catches the things people forget when they\u0026rsquo;re moving fast.\nIn this section:\n🐛 Bug Reports: Make It Reproducible 🔀 Pull Requests: Tell Reviewers What to Look For 💡 Feature Proposals: Start With the Problem 📋 Stories: The Building Block of Delivery How to use these # Copy them into your project and adapt them. Remove sections that don\u0026rsquo;t apply to your team. Add sections that do. The goal is consistency, not compliance. If everyone on the team uses the same structure, reading and reviewing each other\u0026rsquo;s work gets dramatically faster.\nTemplates reduce back-and-forthThe most common reason a ticket or PR needs revision isn\u0026rsquo;t that the work is wrong. It\u0026rsquo;s that the author forgot to include context that the reviewer needs. A template with the right prompts catches this at writing time instead of review time. Less back-and-forth, faster cycle times. Don\u0026#39;t let templates become a formalityIf people are filling in template sections with placeholder text just to satisfy the format, the template is working against you. Every section should earn its place. If nobody reads the \u0026ldquo;Testing\u0026rdquo; section on your feature proposals, remove it. A half-filled template is worse than no template because it trains people to ignore the structure entirely. ","externalUrl":null,"permalink":"/handbook/templates/","section":"Dev Handbook","summary":"Ready-to-use templates for bugs, PRs, feature proposals, and stories. Copy them, adapt them, ship faster.","title":"🧾 Templates: Don't Start From a Blank Page","type":"handbook"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"Career","type":"tags"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/","section":"eval ( code )","summary":"","title":"eval ( code )","type":"page"},{"content":"There\u0026rsquo;s a popular take right now that AI eliminates the need for junior engineers. The argument goes: if an AI agent can do the work of a junior developer, why would you hire one? Just give your senior engineers better tools and let them do the work of three people.\nI get why that looks good on a spreadsheet. I also think it\u0026rsquo;s dangerously short-sighted.\nAnd no, I\u0026rsquo;m not going to make the mentorship argument. I\u0026rsquo;m not going to make the \u0026ldquo;moral obligation to train the next generation\u0026rdquo; argument. Those things matter, but they\u0026rsquo;re not what keeps a CFO up at night.\nLeverage is what keeps a CFO up at night. And if you stop hiring junior engineers, you just handed all of it to your senior workforce.\nJunior employees are salary insurance # Senior engineers know things. They have context, relationships, institutional knowledge. That makes them valuable. It also makes them expensive, and it gives them leverage.\nEventually this turns into a comp conversation. A senior engineer says \u0026ldquo;I want a 40% raise or I\u0026rsquo;m leaving,\u0026rdquo; and the company\u0026rsquo;s ability to respond depends entirely on what their alternatives look like. If there\u0026rsquo;s a bench of mid-level engineers who\u0026rsquo;ve been growing into that space for the past two years, the company has options. They can negotiate from a position of strength. The loss would hurt, but it wouldn\u0026rsquo;t be catastrophic.\nIf there\u0026rsquo;s no bench, because you stopped hiring juniors three years ago and there\u0026rsquo;s nobody coming up behind your seniors, you don\u0026rsquo;t have options. You pay the 40%, or you lose the person and spend six months (and a recruiter\u0026rsquo;s fee) trying to find a replacement at market rate, which is probably even higher.\nJunior employees aren\u0026rsquo;t just doing junior work. They\u0026rsquo;re a long-term bet. They\u0026rsquo;re future mid-levels and future seniors growing inside your organization, building context that you can\u0026rsquo;t hire in from the outside. Every junior you don\u0026rsquo;t hire today is a senior you\u0026rsquo;ll have to overpay for in three years.\nThe pipeline problem is already here # This isn\u0026rsquo;t theoretical. We\u0026rsquo;re watching it play out right now with the boomer generation retiring.\nSmall and mid-sized businesses across the country are closing, not because the business failed, but because the owner is retiring and there\u0026rsquo;s nobody to hand the keys to. They spent decades not investing in their talent pipeline, and when it\u0026rsquo;s time to step away, the business just ends. Everything the owner built, gone. Not because of competition or market shifts. Because nobody was coming up behind them.\nThis is why the apprenticeship model existed for centuries. It was never about getting cheap labor from a teenager sweeping the shop floor. It was about the lifeblood of the business. The master trained the apprentice because without that pipeline, the trade dies when the master does. Every generation of skilled workers has to produce the next one. That\u0026rsquo;s not some feel-good mentorship thing. It\u0026rsquo;s survival.\n\u0026ldquo;But we won\u0026rsquo;t need the next generation because AI will do the work.\u0026rdquo; Fine. If AI replaces ALL engineering work, seniors included, then sure, the pipeline doesn\u0026rsquo;t matter. But nobody is actually arguing that. The argument is that AI replaces juniors specifically.\nWhich means you still need seniors.\nSo where do they come from? They don\u0026rsquo;t show up fully formed. They start as juniors and grow into the role over years. Cut the pipeline and you cut the supply of the very people you\u0026rsquo;re saying you still need. Ask anyone trying to hire a COBOL engineer right now. The pipeline dried up decades ago, and the few who are left name their price.\nA CEO who neglects that pipeline to juice quarterly growth is not doing right by their shareholders. Short-term headcount savings look great on this quarter\u0026rsquo;s earnings call. They look a lot less great when your senior engineers start retiring and you have nobody to replace them.\nThe \u0026ldquo;AI replaces juniors\u0026rdquo; crowd is proposing the exact same mistake on an accelerated timeline. Stop hiring juniors in 2026, and by 2030 you have a workforce of expensive seniors with no succession plan. Some of those seniors will leave for better offers. Some will burn out. Some will just decide they\u0026rsquo;re done.\nYour senior engineers might not need the job # Here\u0026rsquo;s where this gets really interesting for engineering specifically.\nSoftware engineering is one of the few professions where FIRE (Financial Independence, Retire Early) is not theoretical. A senior engineer who bought before housing went through-the-roof, spent a decade earning public-company equity, and kept living like the startup days is in a very different bargaining position from someone still trying to build savings. They don\u0026rsquo;t need the job in the same way. They work because they want to, not because they have to.\nThat changes the power dynamic completely.\nWhen a senior engineer who needs their paycheck asks for a raise, there\u0026rsquo;s a negotiation. Both sides have something to lose. But when a senior engineer who\u0026rsquo;s already financially independent asks for a raise, there\u0026rsquo;s no negotiation. They\u0026rsquo;re not bluffing when they say \u0026ldquo;I\u0026rsquo;ll walk.\u0026rdquo; They will literally retire to a beach and write open source projects for fun. You have nothing to hold over them.\nNow imagine your entire senior engineering team is made up of people like this, and you have no junior pipeline coming up behind them. You\u0026rsquo;re not managing a workforce. You\u0026rsquo;re managing a group of volunteers. Highly paid volunteers who know exactly how much it would cost to replace them. Good luck with that annual review cycle.\nThe only counterbalance is having a healthy pipeline of less experienced engineers who are growing into those roles. People who are building careers, who have financial motivation to stay and grow, who give you organizational resilience when a senior decides they\u0026rsquo;d rather go sailing.\nAI will keep getting better. So what? # I wrote an entire post about why the timeline on that is longer than people think.\nBut even if AI does eventually handle most of what junior engineers do today, that doesn\u0026rsquo;t eliminate the economic argument. It just changes what \u0026ldquo;junior\u0026rdquo; means. Junior engineers of the future might spend less time writing boilerplate and more time reviewing AI output, learning system design, and building the judgment that makes senior engineers valuable. The role evolves. The need for a pipeline doesn\u0026rsquo;t.\nThe hard part is that the old apprenticeship path probably does break. You can\u0026rsquo;t just hand a junior the boilerplate work that AI now handles and pretend nothing changed. Companies have to design a new path: reviewing AI output, tracing why a generated change is wrong, learning the codebase well enough to know when the agent is making a plausible mess, and sitting close enough to senior engineers to absorb judgment instead of just syntax.\nThat\u0026rsquo;s not cheaper in the first quarter. It takes real management attention. But the alternative is pretending senior engineers appear fully formed because the bottom of the ladder got automated. They don\u0026rsquo;t.\nCompanies that stop investing in their pipeline because \u0026ldquo;AI will handle it\u0026rdquo; are making a bet that AI will be good enough, cheap enough, and reliable enough to replace the entire bottom of their talent funnel, permanently, starting now. That\u0026rsquo;s a bold bet. And if they\u0026rsquo;re wrong, they\u0026rsquo;ve lost years of talent development that they can\u0026rsquo;t get back.\nWhat this actually looks like in practice # I\u0026rsquo;m not saying every company needs to hire the same number of juniors they did five years ago. AI is changing the ratio. A senior engineer with good AI tools probably does absorb some of the tasks that used to go to juniors. That\u0026rsquo;s real.\nBut \u0026ldquo;changing the ratio\u0026rdquo; is very different from \u0026ldquo;eliminating the role.\u0026rdquo; Shopify gets this. We significantly expanded early-career hiring for 2026. This is a company betting hard on AI across the board, and we\u0026rsquo;re still investing in the pipeline because those aren\u0026rsquo;t opposing strategies.\nWithin five years, the companies that stopped hiring juniors will be the ones posting breathless LinkedIn articles about their \u0026ldquo;talent pipeline crisis\u0026rdquo; and wondering how it happened. The rest of us will know exactly how it happened. We watched them do it to themselves in real time.\n","date":"26 April 2026","externalUrl":null,"permalink":"/posts/if-you-stop-hiring-juniors-your-seniors-own-you/","section":"Posts","summary":"The ‘AI replaces junior engineers’ argument ignores basic economics. Junior employees aren’t just cheap labor. They’re salary insurance, pipeline protection, and the only hedge companies have against a senior workforce that increasingly doesn’t need the job.","title":"If You Stop Hiring Juniors, Your Senior Engineers Own You","type":"posts"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/tags/leadership/","section":"Tags","summary":"","title":"Leadership","type":"tags"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"26 April 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"5 April 2026","externalUrl":null,"permalink":"/tags/process/","section":"Tags","summary":"","title":"Process","type":"tags"},{"content":"The best engineers treat every project like someone just handed them their kid for the evening.\nThat sounds dramatic until you\u0026rsquo;ve worked with both kinds of engineers: the ones who immediately understand the stakes, and the ones who fill you with anxiety and regret the second you trust them with anything important.\nHere\u0026rsquo;s the babysitter parable.\nThe babysitter parable # It\u0026rsquo;s a Saturday night. You show up at 6. The mom meets you at the door and she looks exhausted. Not \u0026ldquo;long day at work\u0026rdquo; exhausted. \u0026ldquo;I haven\u0026rsquo;t slept more than three hours in a row for two weeks\u0026rdquo; exhausted. The toddler\u0026rsquo;s been sick. She starts walking you through the routine and you can tell she\u0026rsquo;s fighting the urge to just cancel the whole evening and stay home.\nShe shows you where the medicine is. She shows you the pediatrician\u0026rsquo;s number on the fridge. She tells you the baby\u0026rsquo;s been running a low fever but it broke this morning. She says all of this while glancing back at the nursery every thirty seconds.\nBefore she\u0026rsquo;s even done talking, you\u0026rsquo;re already adjusting. This isn\u0026rsquo;t a \u0026ldquo;put on a movie and chill\u0026rdquo; kind of night. This is a \u0026ldquo;send updates every hour with a photo\u0026rdquo; kind of night. Not because she asked you to. Because you can feel that she needs to see her kid is okay before she can relax at all.\nSo that\u0026rsquo;s what you do. An hour in, you text a photo of the kid eating dinner and smiling. \u0026ldquo;Temperature\u0026rsquo;s normal, we\u0026rsquo;re doing great.\u0026rdquo; She looks at her phone, exhales, and goes back to her evening. Two hours in, another photo. Kid\u0026rsquo;s in pajamas watching a movie, looking sleepy and content. By the third check-in she\u0026rsquo;s stopped hovering over her phone. She trusts you. She can actually enjoy her night.\nNow flip it.\nDifferent Saturday. Different family. The dad opens the door, points at the fridge, says \u0026ldquo;pizza money\u0026rsquo;s on the counter, bedtime\u0026rsquo;s 8:30, have fun\u0026rdquo; and he\u0026rsquo;s out the door before you\u0026rsquo;ve taken your shoes off.\nThis guy doesn\u0026rsquo;t need updates. He needs one quiet night where nobody texts him. You can feel it. If you blow up his phone with hourly status reports, you\u0026rsquo;re not being thorough. You\u0026rsquo;re being annoying. You\u0026rsquo;re the babysitter who doesn\u0026rsquo;t know when to back off.\nSo you send one text around bedtime. \u0026ldquo;Kids are down, everything went great, enjoy your night.\u0026rdquo; That\u0026rsquo;s it. He comes home, the house is clean, the kids are asleep. He books you again next weekend.\nSame job both nights. Completely different situations. The only thing that changed was you paying attention.\nNow think about the terrible babysitter. Maybe they ignored the kid and scrolled their phone all night. Maybe they texted the anxious mom once at 10pm: \u0026ldquo;yeah everything\u0026rsquo;s fine.\u0026rdquo; Maybe they blew up the relaxed dad\u0026rsquo;s phone every twenty minutes with updates he didn\u0026rsquo;t want. Maybe the parents came home to a disaster and the babysitter\u0026rsquo;s defense was \u0026ldquo;well, you didn\u0026rsquo;t tell me not to let them eat ice cream for dinner.\u0026rdquo;\nYou never call that babysitter again. You don\u0026rsquo;t give them a second chance. You don\u0026rsquo;t even think about it. The trust is gone in one evening.\nThis is the same thing that happens at work # Every project you pick up is someone else\u0026rsquo;s baby.\nYour product manager scoped it. Your designer sweated the details. Your stakeholder is betting their quarter on it. They\u0026rsquo;re handing it to you and trusting that you\u0026rsquo;ll care about the outcome the way they do.\nThe question isn\u0026rsquo;t \u0026ldquo;did I complete the ticket?\u0026rdquo; The question is \u0026ldquo;did I treat this project like it mattered?\u0026rdquo;\nHigh-stakes launch with executives watching? More updates. Proactive communication. \u0026ldquo;Here\u0026rsquo;s where I am, here\u0026rsquo;s what I found, here\u0026rsquo;s what I\u0026rsquo;m doing about it.\u0026rdquo; Don\u0026rsquo;t wait to be asked. Silence makes people weird when the stakes are high.\nLow-risk internal tool your team lead handed off because they trust you? Don\u0026rsquo;t create noise just to prove you\u0026rsquo;re busy. Ship it. Flag anything surprising. Show that you understood the assignment.\nI\u0026rsquo;ve worked with engineers on both ends of this. Some people make you calmer the second you hand them something important. Other people go dark for two weeks. Deliver something that doesn\u0026rsquo;t match what was asked for. Miss a critical detail because they didn\u0026rsquo;t bother to understand the context. Treat someone\u0026rsquo;s high-stakes launch like a throwaway task.\nNobody sends you an email that says \u0026ldquo;we\u0026rsquo;ve lost trust in you.\u0026rdquo; That\u0026rsquo;s not how it works. You just stop getting put on the important projects. People stop requesting you by name. The interesting work dries up and you can\u0026rsquo;t figure out why. I\u0026rsquo;ve watched engineers blame politics or favoritism for this and, sure, sometimes those things are real. But a lot of the time the answer is simpler and more annoying: someone handed you their most important project and you didn\u0026rsquo;t take care of it.\nWhen you get this right consistently, the opposite happens. You stop being \u0026ldquo;an engineer on the team\u0026rdquo; and start being the person everyone wants on their project. Product managers request you by name. Your manager fights to keep you in their org when reorgs happen. Not because you\u0026rsquo;re the best coder. Because people trust you.\nNone of this is an engineering skill # This is a people problem. You do not get good at it by accident, and there\u0026rsquo;s no checklist for it. You get good at it by caring enough to pay attention every time someone hands you something that matters to them.\nTreat the work like it matters because to the person who handed it to you, it does.\nThis post is about reading expectations nobody says out loud. The Mechanic Parable in my Dev Handbook covers the other side: what happens when both people have expectations and neither one states them explicitly.\n","date":"5 April 2026","externalUrl":null,"permalink":"/posts/the-best-engineers-read-the-room/","section":"Posts","summary":"The best engineers know how to read the room. This is not really an engineering problem, it’s a people problem. The engineers people trust most are the ones who know when to reassure, when to stay quiet, and how to make others feel understood.","title":"The Best Engineers Read the Room","type":"posts"},{"content":"If you\u0026rsquo;ve spent any time on LinkedIn or tech Twitter lately, you\u0026rsquo;ve probably seen the hot takes: AI will replace most knowledge workers within a year. Software engineers are already obsolete, they just don\u0026rsquo;t know it yet. Your job is on borrowed time.\nTake a breath. The timeline those predictions depend on has a massive assumption baked into it: that AI will keep getting dramatically cheaper, fast enough to make human labor uneconomical across the board. The technology side of that equation might hold up. The economics side almost certainly won\u0026rsquo;t.\nThat\u0026rsquo;s actually good news.\nThe tech is real. The timeline isn\u0026rsquo;t. # To be fair, the trend is real. LLMs have gotten more capable while inference costs have dropped. Smaller models are doing work that required massive models a year ago. If you draw a line through those data points and extend it forward, you get a world where AI is so cheap that hiring a human to do the same work stops making financial sense.\nI get the logic, but it\u0026rsquo;s only looking at the technology. The economics tell a different story. That trend line exists in a world where the infrastructure required to deliver AI at scale has hard physical constraints, and those constraints push costs in the opposite direction.\nHard costs don\u0026rsquo;t follow software curves # Even if AI demand stayed perfectly flat from today (it won\u0026rsquo;t), we\u0026rsquo;d still need to build significantly more data centers. The current infrastructure can\u0026rsquo;t handle existing workloads at the scale companies want to run them.\nBuilding a data center takes, what, two to four years from site selection to operational? And that\u0026rsquo;s if permitting goes smoothly, supply chains hold, you can actually secure the power allocation from your local utility, and you\u0026rsquo;ve greased the right local politicians to fast-track things a bit. You\u0026rsquo;re not deploying code. You\u0026rsquo;re pouring concrete, running power lines, and negotiating water rights.\nAnd securing that power isn\u0026rsquo;t a formality. It\u0026rsquo;s becoming the primary bottleneck. Dominion Energy in Virginia, the largest data center market in the world, has warned that data center power demand could threaten grid reliability and has a multi-year interconnection queue. Georgia Power paused new large load connections entirely, including data centers, because their grid couldn\u0026rsquo;t absorb the demand. These aren\u0026rsquo;t hypothetical concerns. These are utilities telling billion-dollar companies \u0026ldquo;we literally cannot give you the electricity you need right now.\u0026rdquo;\nEach large AI data center consumes as much power as a small city. The IEA projects global data center electricity consumption could double by 2026, exceeding 1,000 TWh. Some providers are exploring nuclear power, not because it\u0026rsquo;s trendy, but because nothing else generates enough baseline capacity to keep up.\nThen there\u0026rsquo;s water. AI inference generates heat. Cooling that heat requires water, and lots of it. Google\u0026rsquo;s 2024 environmental report showed a 17% year-over-year increase in water consumption, largely attributed to AI workloads. Data centers in arid regions are already competing with agriculture and residential use for water access.\nGPU prices have skyrocketed due to demand. High-bandwidth memory, networking equipment, specialized cooling systems, even the skilled labor to build and run these facilities. All of it is in higher demand than the supply chain can comfortably handle. When your input costs rise across the board, your output prices rise too. That\u0026rsquo;s not a market failure. That\u0026rsquo;s how markets work.\nNone of these costs disappear because the models get more efficient. You still need the building. You still need the power. You still need the cooling. These are hard costs with hard timelines, and they don\u0026rsquo;t care about your software release cycle.\nThis market isn\u0026rsquo;t what you think it is # Here\u0026rsquo;s something most people don\u0026rsquo;t factor in: the AI token prices you see today are almost certainly subsidized.\nOpenAI shut down Sora, their video generation tool, because the compute costs of running it at scale were unsustainable. They even said they were reallocating Sora\u0026rsquo;s servers to higher-priority workloads. Think about that. That\u0026rsquo;s a company admitting it doesn\u0026rsquo;t have enough infrastructure to run everything it wants to run. The supply constraint isn\u0026rsquo;t theoretical. It\u0026rsquo;s in their own blog post.\nWhen you see that, it\u0026rsquo;s hard not to conclude these are growth-phase prices. The same playbook Uber used with rides, DoorDash used with delivery, and every SaaS company uses during growth mode. Get users hooked at an artificially low price, then raise prices once switching costs are high enough. Each time a product like Sora gets pulled, it tells you the true cost of running these models isn\u0026rsquo;t what\u0026rsquo;s on the tin.\nNow, Uber held prices artificially low for the better part of a decade. Could AI companies do the same? Maybe. But there\u0026rsquo;s a key difference. Uber\u0026rsquo;s subsidy only required cash. The infrastructure already existed: roads, cars, drivers. You can fund below-cost rides with venture money as long as investors keep writing checks. AI companies are trying to subsidize prices while simultaneously building the infrastructure to deliver the product. That\u0026rsquo;s a much more expensive position to hold, and the longer demand keeps growing, the harder it gets to maintain.\nAt some point, token prices will have to reflect actual costs. And actual costs, as we\u0026rsquo;ve established, will be pushed upward by infrastructure constraints.\nThe people projecting that AI will be \u0026ldquo;10x cheaper next year\u0026rdquo; are extrapolating from prices that were never real to begin with.\nEfficiency gains won\u0026rsquo;t outrun demand # OK, here\u0026rsquo;s the thing. Inference efficiency HAS improved dramatically. Model distillation, quantization, better architectures, new silicon like the RISC-V AI chips that promise to make everything 10x cheaper. Some of that will deliver real gains. And I keep seeing people point to those gains as proof that costs will keep dropping forever.\nI\u0026rsquo;ve watched this exact movie before. I know how it ends.\nRemember when companies started moving to cloud infrastructure? The pitch was compelling: compute and storage costs are dropping, so moving to the cloud will save you money. And per-unit costs did drop. The price of a virtual machine or a gigabyte of storage fell steadily.\nBut here\u0026rsquo;s what actually happened. Because compute was cheaper, companies used dramatically more of it. Workloads that weren\u0026rsquo;t economical before suddenly were. Teams spun up environments they never would have provisioned in an on-prem world. I\u0026rsquo;ve been in the room for these conversations at multiple companies. The per-unit cost goes down, everyone celebrates, and then the annual cloud bill comes in 40% higher than last year because consumption exploded.\nToday, most companies are spending more on their P\u0026amp;L for cloud costs than they ever spent on data centers. The per-unit savings were real, but consumption grew faster. Every single time.\nEconomists call this Jevons Paradox: when you make a resource more efficient to use, total consumption often increases rather than decreases. It happened with coal in the 19th century, fuel efficiency in cars, cloud computing, and it\u0026rsquo;s happening right now with AI.\nEvery efficiency gain in AI makes new use cases economical. Cheaper tokens mean companies run AI on tasks they wouldn\u0026rsquo;t have considered a year ago. That drives total demand up. And when total demand grows faster than efficiency improves, the infrastructure constraints don\u0026rsquo;t ease. They intensify. A 10x efficiency gain doesn\u0026rsquo;t help if demand grows 100x.\nThe efficiency gains are real. They just have to outrun the demand they create, which is not a trivial task.\nThe math companies will have to do # So what happens when infrastructure constraints push AI costs up while human labor costs stay relatively flat?\nCompanies start doing arithmetic.\nA senior engineer costs roughly $250,000 to $350,000 fully loaded (salary, benefits, taxes, equipment, management overhead). That\u0026rsquo;s about $120 to $170 per hour of productive work. Right now, AI can do certain tasks at a fraction of that cost. The gap is wide enough that replacing human work with AI is a financial no-brainer for those tasks.\nBut that gap depends on token prices staying low. If infrastructure pressure doubles the delivered cost of AI, and demand keeps pushing it higher, the math starts to shift. Not for everything at once, but task by task, role by role.\nThe green dotted line shows what optimists project: AI costs approaching zero as technology improves. The orange line shows delivered cost when you factor in infrastructure constraints: it bottoms out and curves back up. As it approaches the human labor band, companies start weighing whether AI is actually cheaper than people for specific tasks. I\u0026rsquo;ve already been in this room. A team wants to use AI for a workflow, someone runs the numbers on token volume at scale, and the room gets quiet. The per-request cost looks trivial. The monthly cost at production volume doesn\u0026rsquo;t. Right now those conversations end with \u0026ldquo;the prices will come down.\u0026rdquo; Within a year or two, some of those conversations will end with \u0026ldquo;let\u0026rsquo;s just hire someone.\u0026rdquo;\nWe\u0026rsquo;re not at that inflection point broadly yet. For many tasks, AI is still dramatically cheaper. But at the pace adoption is accelerating, and with infrastructure struggling to keep up, we may see token costs plateau or rise sooner than anyone\u0026rsquo;s projecting.\nWhen that happens, AI stops being a blanket replacement and becomes a cost-benefit decision made task by task. Some work will still be cheaper with AI. Some will be cheaper with people. Most will end up as a blend. And that blend looks a lot less like \u0026ldquo;your job disappears overnight\u0026rdquo; and a lot more like \u0026ldquo;your job changes gradually.\u0026rdquo; The difference matters. Gradual change gives you time to adapt.\nEdge AI doesn\u0026rsquo;t solve this either # Apple Intelligence, local LLMs, on-device inference. If AI moves to the edge, doesn\u0026rsquo;t that route around the data center bottleneck entirely?\nPartially. But anyone who\u0026rsquo;s shipped software across device fragmentation is already wincing. Moving AI to the edge doesn\u0026rsquo;t simplify the problem. It fragments it. Now instead of one 400-billion-parameter model running in a controlled data center environment, you have a 3-billion-parameter model running on seventeen different phone chipsets, four tablet variants, and a laptop that\u0026rsquo;s two OS versions behind. Each one behaves slightly differently. Each one has different memory constraints, thermal limits, and failure modes.\nThat\u0026rsquo;s not fewer problems for humans to solve. That\u0026rsquo;s dramatically more, spread across more environments, with less margin for error. On-device models are less capable by definition, which means more careful prompt engineering, more fallback logic, more testing across hardware, and more human judgment about where the edge model is good enough and where you still need to call home to the data center.\nThe pitch is \u0026ldquo;AI moves closer to the user and everything gets simpler.\u0026rdquo; The reality is that distributing AI across millions of heterogeneous devices is one of the harder engineering problems you can take on. Anyone who\u0026rsquo;s shipped software across device fragmentation knows this in their bones. That\u0026rsquo;s the kind of problem that creates work, not eliminates it.\nWhat this actually means for your career # The pace at which AI replaces human work is governed by economics, not just capability. And the economics are about to get more complicated, not simpler. Infrastructure constraints, rising input costs, subsidized pricing that can\u0026rsquo;t last, and demand outpacing supply all put upward pressure on the cost of AI. That upward pressure is the natural brake that slows the \u0026ldquo;replace everyone\u0026rdquo; timeline.\nYour career has more runway than the doomers suggest. Not infinite runway, but enough to adapt. Enough to learn how to work alongside these tools. Enough to position yourself as someone who makes AI more effective rather than someone AI makes redundant.\nThe people telling you to panic are either modeling the technology curve in isolation (which is a polite way of saying they\u0026rsquo;re wrong) or they have a financial interest in you believing the timeline is shorter than it is. The real world has data centers to build, power grids to expand, water to allocate, and supply chains to scale. All of that takes time. Human time, the kind that doesn\u0026rsquo;t compress no matter how smart the model gets.\nAt some point, all technology has to deal with the realities of human-scale problems. It\u0026rsquo;s naive to project timelines as if technology scales unimpacted by the physical world. The longest tail in any technology revolution isn\u0026rsquo;t the innovation curve. It\u0026rsquo;s the infrastructure curve.\nAnd right now, that curve is working in your favor.\n","date":"30 March 2026","externalUrl":null,"permalink":"/posts/moores-law-wont-set-your-career-timeline/","section":"Posts","summary":"Everyone assumes AI will replace most jobs within a year. The technology might be ready, but the economics aren’t. Data centers, power grids, and component costs don’t follow Moore’s Law. When demand outstrips infrastructure, token prices go up, and companies start doing the math on AI versus people.","title":"Moore's Law Won't Set Your Career Timeline","type":"posts"},{"content":"I wrote recently about how Agile is bending under the weight of AI-assisted development. The unit of work is getting larger, iterations are happening on complete features instead of thin slices, and the old assumptions about how to decompose work are shifting.\nThere\u0026rsquo;s a sneakier problem hiding inside that shift. When every next feature is just one more prompt away, the hardest part of the job isn\u0026rsquo;t building. It\u0026rsquo;s stopping.\nThe \u0026ldquo;one more prompt\u0026rdquo; trap # Before AI, projects had a natural governor built in. The next task on the backlog would take a day, maybe two. That friction created a decision point. Is this feature worth the time? Does it need to ship now, or can it wait? The cost of implementation forced the conversation.\nAI removed the friction, but the decision point still matters. In fact, it matters more.\nWhen your AI agent can scaffold a new feature in fifteen minutes, \u0026ldquo;one more prompt\u0026rdquo; feels harmless. You already built the notification preferences system. Why not add granular per-channel controls? That\u0026rsquo;s just one more prompt. Why not add a digest mode? Another prompt. Why not add a scheduling UI so users can set delivery windows? The agent will have it done before your coffee gets cold.\nEach addition is small and cheap in isolation. But you just turned an MVP notification system into a feature-rich beast that needs testing, documentation, edge case handling, and ongoing maintenance. None of that was in the original scope. You didn\u0026rsquo;t plan for it. You just kept prompting.\nThis is Parkinson\u0026rsquo;s Law applied to code. Work expands to fill the capacity available. Give a team more time and they\u0026rsquo;ll find ways to use it. Give a developer an AI agent that makes building nearly free, and they\u0026rsquo;ll keep building past the point where they should have shipped.\nEstimation is uncharted territory # This creates a genuine problem for scoping and estimation. The old heuristic of complexity equals time is breaking down. Tasks that used to take a week might take a day. Tasks that seemed trivial might take longer because the AI generates a plausible-looking solution that fails in subtle ways. Even experienced engineers struggle to predict timelines when the relationship between complexity and effort keeps shifting underneath them.\nDoes AI cut every project to half the time? A third? A fifth? The honest answer is: nobody knows yet. It depends on the task, the codebase, the model, and whether the problem is greenfield or tangled up in production constraints. New models keep pushing the boundary of what\u0026rsquo;s \u0026ldquo;just one prompt away,\u0026rdquo; which means any estimate you make today might be wildly wrong in six months.\nWhat we do know is that the old safety valve is gone. When a hard task took a week, you had a week of natural friction preventing scope creep. When that same task takes an afternoon, you have the rest of the week to keep adding things nobody asked for.\nEngineers as taste makers # This is where the role of the engineer changes in a way that most teams haven\u0026rsquo;t caught up to yet.\nWhen building is cheap, the valuable skill isn\u0026rsquo;t building. It\u0026rsquo;s taste. Knowing what belongs in the product and what doesn\u0026rsquo;t. Knowing when a feature is complete enough to ship and when you\u0026rsquo;re gold-plating. Knowing the difference between \u0026ldquo;this needs one more iteration\u0026rdquo; and \u0026ldquo;this is done, I just want to keep tinkering.\u0026rdquo;\nThat\u0026rsquo;s a product sense skill, not a technical skill. And it\u0026rsquo;s one that most engineering cultures don\u0026rsquo;t develop deliberately. We train engineers to solve problems, not to decide which problems are worth solving right now. We celebrate thoroughness and completeness, not the discipline to ship something 80% polished because the last 20% doesn\u0026rsquo;t matter yet.\nThe engineers who thrive in an AI-assisted world will be the ones who can look at a working feature and say \u0026ldquo;this is the MVP, ship it.\u0026rdquo; Not because they can\u0026rsquo;t make it better, but because they understand that shipping something good today beats shipping something perfect next week. That\u0026rsquo;s always been true in theory. AI makes it true in practice every single day, because \u0026ldquo;next week\u0026rsquo;s perfect version\u0026rdquo; is now \u0026ldquo;two hours from now\u0026rdquo; and the temptation to chase it is constant.\nThe real definition of done # Every process framework has a \u0026ldquo;definition of done.\u0026rdquo; Most teams treat it as a checklist: tests pass, code reviewed, documentation updated. That checklist doesn\u0026rsquo;t protect you from scope creep driven by cheap implementation.\nWhat teams need now is a \u0026ldquo;definition of enough.\u0026rdquo; Before you start building, answer: what does this feature need to do to be shippable? Not ideal. Not complete. Shippable. Write that down. When the AI agent hands you a working implementation that meets that bar, ship it. Resist the pull of one more prompt.\nThis is harder than it sounds. Every engineer has felt the gravitational pull of \u0026ldquo;while I\u0026rsquo;m in here, I might as well\u0026hellip;\u0026rdquo; That instinct used to be moderated by the cost of implementation. Now the cost is near zero, so the only thing standing between a clean MVP and a bloated feature is the engineer\u0026rsquo;s judgment.\nThat judgment, the ability to decide when something is fully baked and to ship the thing, is becoming one of the most valuable skills on any engineering team. It\u0026rsquo;s not a skill you can automate. It\u0026rsquo;s not a skill the AI agent has. It\u0026rsquo;s a fundamentally human call about what\u0026rsquo;s good enough, and making it well requires understanding the user, the business context, and the maintenance cost of everything you add.\nBuild less, ship more # The teams that will win in an AI-assisted world aren\u0026rsquo;t the ones that build the most. They\u0026rsquo;re the ones that ship the right amount. They\u0026rsquo;ll treat their AI agents like a kitchen with an unlimited pantry: just because you can make twelve courses doesn\u0026rsquo;t mean the dinner needs them.\nThe bottleneck was never the code. It still isn\u0026rsquo;t. Now the bottleneck is the discipline to stop writing code and start shipping it.\n","date":"24 March 2026","externalUrl":null,"permalink":"/posts/the-hardest-skill-in-ai-assisted-development-is-knowing-when-to-stop/","section":"Posts","summary":"AI makes ‘one more prompt’ irresistible. The next feature is always ten minutes away. The hardest engineering skill in the age of AI isn’t building. It’s deciding when to ship.","title":"The Hardest Skill in AI-Assisted Development is Knowing When to Stop","type":"posts"},{"content":"I wrote recently about how AI exposed the process debt teams have been ignoring. The short version: code got cheap, and suddenly the bottleneck is scoping, estimation, and communication. All the human skills that teams treated as optional for years.\nBut there\u0026rsquo;s a second-order effect I didn\u0026rsquo;t get into. It\u0026rsquo;s not just that process matters more now. It\u0026rsquo;s that the unit of work itself is changing shape.\nWhen rewriting is cheaper than refining # Agile\u0026rsquo;s core promise is small, incremental slices. Build a little, ship a little, learn, adjust. That model assumes writing code is expensive. The assumption drives everything: small stories, thin vertical slices, iterative enhancement over multiple sprints. You build the minimum, get feedback, and layer on improvements because starting over would be wasteful.\nBut what happens when starting over takes ten minutes?\nWith AI-assisted development, generating a complete feature implementation is fast and cheap. If you get the approach wrong, you don\u0026rsquo;t carefully refactor your way to a better design. You throw it away and regenerate with a better prompt. The cost of a wrong decision dropped from \u0026ldquo;days of rework\u0026rdquo; to \u0026ldquo;another conversation with your AI agent.\u0026rdquo;\nThis changes the shape of how work actually gets done. Instead of building a feature in five thin slices across five sprints, you write the whole thing in one pass. Then you iterate on that complete implementation, adjusting the design, reworking the approach, regenerating entire components when they don\u0026rsquo;t fit. It\u0026rsquo;s not waterfall in the traditional sense, because you\u0026rsquo;re still iterating and responding to feedback. But it\u0026rsquo;s not the classic Agile slice-and-dice either.\nI\u0026rsquo;ve been calling it \u0026ldquo;Agile Waterfall\u0026rdquo; in my head, and the name keeps fitting.\nWhat Agile Waterfall looks like in practice # Let\u0026rsquo;s say you\u0026rsquo;re building a new notification preferences system. In traditional Agile, you\u0026rsquo;d break that into stories: \u0026ldquo;user can toggle email notifications,\u0026rdquo; \u0026ldquo;user can set quiet hours,\u0026rdquo; \u0026ldquo;user can choose notification channels per event type.\u0026rdquo; Each one ships independently. Each one gets its own review cycle.\nWith AI, the natural workflow looks different. You describe the entire notification preferences system to your agent. It generates the data model, the UI, the API endpoints, and the test suite in one shot. Maybe that first pass gets the data model wrong, or the UX doesn\u0026rsquo;t feel right. So you iterate on the whole feature. You might regenerate the entire backend with a different schema. You might throw away the UI and start fresh with a different component library. Each iteration is a complete, working implementation.\nThe feedback loop is still there. You\u0026rsquo;re still learning and adjusting. But the unit of iteration isn\u0026rsquo;t a thin slice of functionality. It\u0026rsquo;s the entire feature.\nThe production boundary # Here\u0026rsquo;s where this falls apart: code that\u0026rsquo;s already running in production.\nYou can\u0026rsquo;t throw away and regenerate a feature that has users, data, and integrations depending on it. Production code comes with migration paths, backward compatibility requirements, and the accumulated weight of real usage patterns. An AI agent can regenerate your notification preferences system from scratch, but it can\u0026rsquo;t magically migrate 50,000 users\u0026rsquo; existing preference records to a new schema without careful, incremental work.\nThis is where traditional Agile still earns its keep. Refactoring production systems, evolving existing APIs, migrating data: these are inherently incremental problems. You can\u0026rsquo;t \u0026ldquo;regenerate\u0026rdquo; your way out of a schema migration. You need small, safe, reversible changes deployed one at a time with rollback plans.\nSo the picture isn\u0026rsquo;t \u0026ldquo;Agile is dead.\u0026rdquo; It\u0026rsquo;s more nuanced than that. For greenfield work and new features, AI pushes teams toward larger, more complete iterations. For production systems, the incremental discipline of Agile is still the safest approach. Most teams are doing both simultaneously, which means the process needs to flex.\nThe ticket size problem # This shift creates a practical tension that engineering leaders need to think about now. If AI makes it natural to work on entire features in one pass, then the traditional advice of \u0026ldquo;break your tickets down into the smallest possible unit of work\u0026rdquo; starts to fight the grain.\nI\u0026rsquo;ve seen this already on teams using AI heavily. Engineers pick up a well-decomposed set of tickets and end up implementing three or four of them in a single AI session because the agent doesn\u0026rsquo;t think in thin slices. It generates the whole thing. The engineer then spends more time figuring out which parts to commit against which ticket than they spent on the actual implementation.\nThat\u0026rsquo;s process serving itself instead of serving the team.\nThe likely correction is larger tickets that describe complete features or capabilities, with iteration happening within the ticket rather than across a series of tiny ones. The definition of \u0026ldquo;done\u0026rdquo; doesn\u0026rsquo;t change. The size of what gets done in one pass does.\nWhat I think happens next # I don\u0026rsquo;t think Agile goes away. The principles of iterating based on feedback, delivering working software frequently, and responding to change are timeless. Those ideas predate the Agile Manifesto and they\u0026rsquo;ll outlast whatever comes next.\nBut I do think the specific practices built on top of those principles are going to shift. Story pointing, sprint planning, and task decomposition were all calibrated for a world where writing code was the bottleneck. That\u0026rsquo;s no longer true for a growing share of development work. The bottleneck is moving to design decisions, integration testing, and production operations.\nMy bet: we\u0026rsquo;ll see best practices adapt. Teams that lean into AI will naturally gravitate toward larger units of work per ticket, fewer but more complete iterations, and more emphasis on the parts that AI can\u0026rsquo;t shortcut (production migrations, system integration, user feedback loops). The Agile Manifesto\u0026rsquo;s values survive. The specific rituals and ticket structures that grew up around those values? Those are going to look very different in two years.\nIf I\u0026rsquo;m being honest about what I wish would happen, it looks a lot like what 37signals described in Shape Up. Fixed time appetites instead of open-ended sprints. Shaped work that defines the boundaries of a problem without prescribing the implementation. Teams that own the entire scope and make their own tradeoffs within the appetite. That model already aligns with how AI-assisted work naturally flows: here\u0026rsquo;s the shaped problem, here\u0026rsquo;s the time box, go build the whole thing and iterate until it fits. Whether the industry actually moves in that direction or just bolts AI onto existing Scrum ceremonies is an open question. I know which outcome I\u0026rsquo;m betting on, but I\u0026rsquo;ll admit it might be more wish than prediction.\nThe teams that figure this out first will ship faster. The teams that force AI-assisted work into 2015-era Scrum processes will wonder why their velocity metrics look great but their engineers feel like they\u0026rsquo;re fighting the process instead of using it.\n","date":"16 March 2026","externalUrl":null,"permalink":"/posts/agile-is-bending-not-breaking-in-the-age-of-ai/","section":"Posts","summary":"AI makes writing code so cheap that iterative enhancement stops making sense. Instead of refining small slices, teams are regenerating entire features and iterating on the whole thing. Agile isn’t dead, but its assumptions are bending hard.","title":"Agile is Bending, Not Breaking, in the Age of AI","type":"posts"},{"content":"","date":"9 March 2026","externalUrl":null,"permalink":"/tags/architecture/","section":"Tags","summary":"","title":"Architecture","type":"tags"},{"content":"","date":"9 March 2026","externalUrl":null,"permalink":"/tags/ruby/","section":"Tags","summary":"","title":"Ruby","type":"tags"},{"content":"","date":"9 March 2026","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"I maintain Warden, the authentication library behind Devise in Ruby on Rails, originally created with Daniel Neighman. It has over 125 million downloads. It authenticates millions of users every day. I later worked at NCC Group, one of the largest cybersecurity firms in the world, building products designed to withstand nation-state attacks.\nHere\u0026rsquo;s the thing I wish every engineering leader understood: the biggest mistake teams make with auth isn\u0026rsquo;t the initial implementation. It\u0026rsquo;s underestimating the maintenance.\nThe build is the easy part # Building an authentication system is a well-documented problem. There are tutorials, patterns, and libraries for every language and framework. A competent engineer can wire up password hashing, session management, and JWT tokens in a week. It will work. It will pass code review. It will ship.\nAnd then it will sit there for years.\nThat\u0026rsquo;s where the real cost lives. Authentication isn\u0026rsquo;t a feature you build once. It\u0026rsquo;s infrastructure you maintain forever. Every zero-day vulnerability in a hashing algorithm, every new attack vector against JWTs, every CVE in a dependency you forgot you had: all of it is your problem now. And the team that built the auth system three years ago? Half of them have left the company.\nNobody staffs for zero-days # Here\u0026rsquo;s the pattern I see over and over. A team builds auth in-house because \u0026ldquo;our requirements are unique\u0026rdquo; or \u0026ldquo;we don\u0026rsquo;t want the dependency.\u0026rdquo; The implementation is solid. Everyone moves on to product work. Six months later, a vulnerability drops. Maybe it\u0026rsquo;s an algorithm confusion attack or a signature forgery vulnerability in your JWT library. Maybe it\u0026rsquo;s a timing side-channel in the password comparison. Maybe it\u0026rsquo;s something in a transitive dependency that nobody realized was even in the stack.\nWho fixes it? The original team has moved on. The engineers who understand the auth code at a deep level are now building other things. There\u0026rsquo;s no standing team responsible for authentication maintenance, because nobody planned for that. Auth was a project, not a product.\nWith a well-maintained community library, that zero-day is someone else\u0026rsquo;s full-time problem. Not because you\u0026rsquo;re being lazy, but because a library with millions of users has millions of reasons to find and fix vulnerabilities fast. The odds of your internal team spotting a subtle attack vector before a community of thousands? Slim to none.\n\u0026ldquo;Just throw an AI agent at it\u0026rdquo; # I can already hear the counterargument: \u0026ldquo;We\u0026rsquo;ll set up an AI agent to monitor our auth dependencies for vulnerabilities. Problem solved.\u0026rdquo;\nNo. Auth security requires deterministic, auditable decisions, not probabilistic ones. An AI agent can hallucinate that a CVE doesn\u0026rsquo;t apply to your usage. It can miss a subtle attack vector because it wasn\u0026rsquo;t in the training data. It can give you false confidence that everything is fine when it isn\u0026rsquo;t.\nAnd when the breach happens, \u0026ldquo;the agent said it was fine\u0026rdquo; doesn\u0026rsquo;t hold up in a postmortem, a compliance audit, or a courtroom. You need to know who reviewed what, when, and why they made the call. That\u0026rsquo;s a human accountability problem, not a monitoring problem.\nAI tooling can help surface issues faster. It\u0026rsquo;s a reasonable part of a security workflow. But it\u0026rsquo;s not a substitute for a team that owns auth as a product and makes deliberate, documented decisions about every vulnerability that comes in. The moment you treat security as something you can fully delegate to automation, you\u0026rsquo;ve created the exact staffing gap this article is warning about, just with a shinier label on it.\nThe JWT checklist that should scare you # Since we\u0026rsquo;re here, let me share the shortlist of JWT pitfalls I\u0026rsquo;ve seen teams miss. These aren\u0026rsquo;t theoretical. They\u0026rsquo;re the ones that go unnoticed for years:\nAlways validate the alg header before processing the body. The alg: none attack is one of the oldest tricks in the book, and I still see implementations that don\u0026rsquo;t check for it. If you\u0026rsquo;re using HS256, consider RS256. Symmetric algorithms are vulnerable to brute force if someone gets hold of a token. Asymmetric algorithms (RS256) eliminate that vector entirely. If you\u0026rsquo;re using elliptic-curve cryptography, verify that public keys are valid curve points and private keys sit inside the valid range. Invalid curve attacks are real. Check every claim: exp, iat, aud, sub, nbf. Don\u0026rsquo;t trust the payload until you\u0026rsquo;ve verified all of them. Skipping one is an invitation for token reuse attacks. Each of these is documented. Each of these is a solved problem in mature libraries. Each of these is something a team rolling their own auth has to discover, implement, and maintain by hand. Forever.\nWhat I\u0026rsquo;d do differently with Warden # Warden has been rock-solid for over a decade. It does exactly what it was designed to do, and it does it reliably. But I learned something uncomfortable about open source: stability gets mistaken for abandonment.\nThe library hasn\u0026rsquo;t had a meaningful change in nearly ten years because it doesn\u0026rsquo;t need one. The authentication layer it provides is complete. But people look at a quiet commit history and assume the project is deprecated. They move on to newer, flashier alternatives that may be less battle-tested but look more \u0026ldquo;active.\u0026rdquo;\nIf I could do it over, I\u0026rsquo;d push out regular releases even when the changes are minor. Better documentation, more use-case examples, clearer communication about what \u0026ldquo;done\u0026rdquo; looks like for a library. The technical lesson is that Warden\u0026rsquo;s architecture was right. The leadership lesson is that perception matters as much as reality, and maintaining trust with your user base requires active communication even when the code doesn\u0026rsquo;t change. That lesson, by the way, applies to a lot more than open source.\nThe decision framework # If you\u0026rsquo;re an engineering leader deciding whether to build auth in-house or use an established library, here\u0026rsquo;s how I\u0026rsquo;d frame it:\nBuild in-house if you have a genuinely unique authentication requirement that no existing library can support, AND you\u0026rsquo;re willing to staff ongoing maintenance for the lifetime of the system, AND you have security expertise on the team to evaluate emerging attack vectors continuously.\nUse an established library if any of those conditions aren\u0026rsquo;t true. Which, for most teams, means use the library.\nThe cost of auth isn\u0026rsquo;t the sprint it takes to build it. It\u0026rsquo;s the decade of vigilance it takes to keep it safe. Plan for that, or let the community carry it for you.\n","date":"9 March 2026","externalUrl":null,"permalink":"/posts/auth-lessons-from-building-warden/","section":"Posts","summary":"After maintaining an authentication library with 125 million downloads for over a decade, here’s what I learned about the real cost of auth: it’s not the build, it’s the maintenance nobody plans for. And no, an AI agent monitoring your dependencies isn’t the answer.","title":"The Real Cost of Rolling Your Own Auth","type":"posts"},{"content":"Code has never been cheaper to produce. AI tools can scaffold a feature in minutes that would have taken a day. They write tests, generate boilerplate, refactor patterns, and autocomplete their way through problems that used to eat an afternoon.\nAnd teams with broken processes are shipping broken software faster than ever.\nThat\u0026rsquo;s the part nobody warned you about. AI is an accelerant. If your team has clear scope, honest estimates, and rituals that actually surface problems, AI makes you dramatically faster. If your team has vague tickets, fictional estimates, and standups where everyone just reads off the board, AI helps you produce more of the wrong thing in less time.\nThe hard part of software was never writing the code. It was figuring out what to build, agreeing on what \u0026ldquo;done\u0026rdquo; means, and keeping everyone pointed in the same direction. Those skills got treated as optional for years. They weren\u0026rsquo;t, but teams could get away with it when the cost of writing code was high enough to slow everything down naturally. That natural brake is gone now. If your process has cracks, AI is going to blow them wide open.\nThe system, not the pieces # Walk into a busy restaurant kitchen. Every cook on the line is talented. They can each make great food. But a great kitchen isn\u0026rsquo;t great because of any one thing. It\u0026rsquo;s the prep list that feeds the line. The ticket system that sequences the orders. The calls between stations that keep timing in sync. The expo checking every plate before it leaves the window. Remove any one of those and the kitchen still functions, just poorly enough that nobody can pinpoint why. The food is good but the timing is off. The timing is fine but the wrong dish goes to the wrong table. Each piece is simple on its own. The system is what makes them a restaurant instead of six people cooking in the same room.\nEngineering teams work the same way. Scoping should feed estimation. Estimation should set expectations. Expectations should drive what you talk about in standup. Standup should surface blockers. The board should reflect all of it. When one link is missing, the whole chain drifts. And the drift is slow enough that you don\u0026rsquo;t notice until someone asks \u0026ldquo;what are we even working on?\u0026rdquo; and nobody has a confident answer.\nNow add AI to that drifting team. The code shows up faster, but the drift accelerates too. You\u0026rsquo;re merging PRs that don\u0026rsquo;t match the ticket because the ticket was vague. You\u0026rsquo;re shipping features that don\u0026rsquo;t solve the problem because nobody scoped the problem clearly. You\u0026rsquo;re moving tickets to \u0026ldquo;done\u0026rdquo; without anyone agreeing on what \u0026ldquo;done\u0026rdquo; meant. The velocity chart looks great. The product doesn\u0026rsquo;t.\nWhat actually breaks # Most teams don\u0026rsquo;t have a broken process. They have half a process. Standups that don\u0026rsquo;t connect to the board. Tickets that don\u0026rsquo;t describe what \u0026ldquo;done\u0026rdquo; looks like. Estimates that nobody trusts, including the person who gave them.\nThe problem is that each piece was adopted in isolation. Someone read a blog post about standups. Someone else copy-pasted a ticket template from a previous job. Now you\u0026rsquo;ve got a Frankenstein workflow where nothing feeds into anything else. And when something slips, nobody can point to where the system broke, because there was no system. Just a collection of practices duct-taped together.\nBefore AI, that duct tape held. Barely. The slowness of writing code gave teams time to course-correct. A developer would start building, realize the ticket was vague, walk over (or Slack over) and ask clarifying questions. That back-and-forth was inefficient, but it was also a safety net. AI removes the safety net. The developer prompts, the code appears, the PR goes up, and nobody paused long enough to ask if this was the right thing to build. And when someone does pause, when a developer actually stops and asks \u0026ldquo;what are we solving here?\u0026rdquo;, the answer comes back vague or conflicting. That\u0026rsquo;s not an AI failure. That\u0026rsquo;s the process failing at speed.\nThe skills that matter now # Every blog post about standups tells you to answer three questions. Very few tell you what to do when the answer to \u0026ldquo;what are you working on?\u0026rdquo; doesn\u0026rsquo;t match what the board says. That\u0026rsquo;s the interesting part. That\u0026rsquo;s where the system either works or falls apart.\nThe same is true for estimation. Everyone argues about story points vs. hours vs. t-shirt sizes. Almost nobody talks about what you\u0026rsquo;re actually measuring, which is confidence and risk, not time. And once you frame it that way, the entire conversation changes. You stop arguing about whether something is a 3 or a 5, and start asking \u0026ldquo;what don\u0026rsquo;t we know yet?\u0026rdquo;\nThat question matters more now than it ever has. When code is cheap, the expensive mistakes are building the wrong thing, solving the wrong problem, and discovering too late that nobody agreed on the goal. Those are all process failures, not technical ones. And they\u0026rsquo;re the failures that AI accelerates.\nYou\u0026rsquo;ll hear people say that models will get better at this. That AI will learn to scope, prioritize, and ask the right questions on its own. Maybe it will. But even perfect AI-generated tickets don\u0026rsquo;t fix a team that doesn\u0026rsquo;t agree on what \u0026ldquo;done\u0026rdquo; means, or doesn\u0026rsquo;t trust its own estimates, or runs standups that nobody listens to. The system that connects scoping to estimation to communication to accountability is a human system. Better models don\u0026rsquo;t build that for you. And when multiple engineers are prompting AI against overlapping or poorly scoped tickets, the agents collide. You get duplicate work, conflicting implementations, and more rework than in the past. The coordination problem gets worse, not better.\nThe skills that separate effective teams from fast-but-chaotic ones were never optional. They just felt optional when everything moved slowly enough to self-correct. Clear scoping. Honest estimation. Rituals that earn their keep. A board that reflects reality. Communication that replaces the hallway conversations remote teams don\u0026rsquo;t have.\nThese are the skills that separate teams that use AI effectively from teams that use AI to ship chaos faster. The code is the easy part. It always was. The difference is that now there\u0026rsquo;s nowhere left to hide.\nBuilding the kitchen # I\u0026rsquo;ve spent more than a decade running this problem at companies from seed stage to Shopify-scale. The details change every time. The principles don\u0026rsquo;t. Write clear tickets. Estimate confidence, not hours. Run rituals that earn their keep. Make the board reflect reality. Hold each other accountable without making it personal.\nI wrote all of it down in a Dev Handbook. It starts with a parable about expectations and works through scoping, rituals (standups, planning, retros, estimation), the daily routine that keeps it all in sync, and templates you can use tomorrow.\nYou don\u0026rsquo;t need to adopt all of it. But if you adopt any of it, the handbook explains how each piece connects to the rest. Because the goal isn\u0026rsquo;t to add more practices. It\u0026rsquo;s to build a kitchen, not just hire better cooks.\n","date":"3 March 2026","externalUrl":null,"permalink":"/posts/ai-exposed-your-process-debt/","section":"Posts","summary":"AI made code cheap. The bottleneck was never the code. It was scoping, estimation, and communication, the skills teams treated as optional for years. That bet no longer pays off.","title":"AI Exposed the Process Debt You've Been Ignoring","type":"posts"},{"content":"","date":"3 March 2026","externalUrl":null,"permalink":"/tags/remote-work/","section":"Tags","summary":"","title":"Remote-Work","type":"tags"},{"content":"Every Ruby codebase I\u0026rsquo;ve worked in has classes littered with public constants. DEFAULT_TIMEOUT, VALID_STATES, QUEUE_NAMES. They feel clean and simple. They\u0026rsquo;re also one of the most common ways teams accidentally couple their code together, making refactoring far more expensive than it needs to be.\nThis post argues that class methods are almost always a better choice than constants for exposing static values. Not because constants are inherently bad, but because they skip the one thing that keeps a codebase maintainable: an intentional API boundary.\nIf you\u0026rsquo;ve ever tried to rename a hash key inside a class and discovered that six other files broke, you\u0026rsquo;ve felt this problem. Constants are the reason.\nRuby constants vs class methods: three approaches # I\u0026rsquo;ll borrow a scenario inspired by Kir Shatrov\u0026rsquo;s post on methods vs. constants, modified to use hashes so the coupling problem is more visible.\nYou\u0026rsquo;re building a class that manages several queues. You need to keep track of each queue by name. Here are three ways you might do it:\n# Option 1 – Constant class Queue QUEUE_NAMES = {reserved: 1, optional: 2} end # Option 2 – Class method class Queue class \u0026lt;\u0026lt; self def get_queue(name) queue_names[name] end private def queue_names @queue_names ||= {reserved: 1, optional: 2} end end end # Option 3 – Both class Queue QUEUE_NAMES = {reserved: 1, optional: 2} def self.queue_names QUEUE_NAMES end end Option 2 is the better design. Let me show you why.\nPerformance: constants vs class methods in Ruby # In older versions of Ruby, class methods that returned strings had a real performance cost because of duplicated string allocation. With frozen string literals, that\u0026rsquo;s no longer true for strings.\nFor non-string values like hashes, there\u0026rsquo;s a small difference: a method returning a hash literal allocates a new hash on every call, while a constant allocates once. You can close that gap with memoization (@queue_names ||= {...}), which is what I\u0026rsquo;d recommend for any value that doesn\u0026rsquo;t need to change between calls. The performance difference is negligible for most code, but it\u0026rsquo;s worth knowing about.\nThe bigger point is that the historical performance argument for constants over methods is largely gone. It explains why so much existing Ruby code defaults to constants, but it\u0026rsquo;s no longer a strong reason to choose them.\nWhy Ruby constants create coupling # Constants are public by default. That means any class in your system can reach into Queue::QUEUE_NAMES and use the raw data structure directly. In most cases, the author never intended for that hash to be part of the class\u0026rsquo;s public contract. But nothing prevents it, and someone will use it.\nHere\u0026rsquo;s a Scheduler that does exactly that:\nclass Scheduler def schedule_job(queue_name, job) queue = Queue::QUEUE_NAMES[queue_name.downcase] run_on(queue, job) end def run_on(queue, job) # ... end end At first glance, this looks fine. It works. But Queue has lost control of its own internal data structure. The Scheduler is now coupled to the shape of that hash, not just the existence of it.\nWatch what happens when Queue needs to evolve. Let\u0026rsquo;s say you need to add priority levels and richer metadata:\nclass Queue QUEUE_NAMES = { urgent: {id: 1, name: \u0026#39;reserved\u0026#39;, priority: 1}, low: {id: 2, name: \u0026#39;optional\u0026#39;, priority: 99} } end This change is completely reasonable. Queue owns the queues, and it should be able to restructure its own data. But Scheduler is now broken because it was counting on QUEUE_NAMES[queue_name] returning an integer, not a hash.\nThis is a leaky abstraction. The constant exposed an internal detail, another class built on top of that detail, and now you can\u0026rsquo;t change one without changing the other. Multiply this across a real codebase and you get the kind of refactoring paralysis where every small change cascades into a dozen files.\nThis is also why senior engineers and tech leads care about API design even for internal classes. It\u0026rsquo;s not academic purity. It\u0026rsquo;s the difference between a codebase where you can confidently ship changes and one where every PR is a minefield.\nHow class methods protect your Ruby API # With Option 2, the Queue class controls what data leaves the building. Consumers interact with a method, not a raw data structure:\nclass Queue class \u0026lt;\u0026lt; self def get_queue(name) queue_names[name] end private def queue_names @queue_names ||= {reserved: 1, optional: 2} end end end Now Scheduler uses the method:\nclass Scheduler def schedule_job(queue_name, job) queue = Queue.get_queue(queue_name.downcase) run_on(queue, job) end def run_on(queue, job) # ... end end When Queue needs to restructure its internals, it can do so without breaking Scheduler at all:\nclass Queue class \u0026lt;\u0026lt; self def get_queue(name) queue = queue_names.find { |_priority, details| details[:name] == name } {queue[:name] =\u0026gt; queue[:priority]} end private def queue_names @queue_names ||= { urgent: {id: 1, name: \u0026#39;reserved\u0026#39;, priority: 1}, low: {id: 2, name: \u0026#39;optional\u0026#39;, priority: 99} } end end end Queue now has complete autonomy over its internal implementation. It can restructure, rename, add fields, or change storage mechanisms. As long as get_queue returns the same shape, nothing downstream breaks. That\u0026rsquo;s what an API boundary gives you.\nMore reasons to prefer class methods over constants # Constants can\u0026rsquo;t be garbage collected. A constant lives in memory for the lifetime of the process. For small hashes this doesn\u0026rsquo;t matter. For large lookup tables or configuration objects, it can. A class method only allocates when called (and with frozen literals, even that\u0026rsquo;s cheap).\nConstants are awkward to override in tests. Ruby does let you redefine a constant, but it throws a warning when you do, and the ergonomics are clunky. RSpec\u0026rsquo;s stub_const exists specifically to work around this. A class method is just a method. You can stub it, mock it, or override it in a subclass with a simple allow(...).to receive(...). Testing becomes straightforward instead of a workaround.\nConstant name collisions are a real footgun. If User::DEFAULT and Account::DEFAULT both exist, it\u0026rsquo;s easy to grab the wrong one, especially in modules or inherited contexts where Ruby\u0026rsquo;s constant lookup walks the ancestor chain. Class methods don\u0026rsquo;t have this problem because the method name is scoped to the class that defines it.\nWhy Option 3 doesn\u0026rsquo;t help # Option 3 (constant plus a wrapper class method) looks like a compromise, but it gives you all the downsides of Option 1 with none of the protection of Option 2. The constant is still public. Any code can bypass the method and access Queue::QUEUE_NAMES directly. The method is just a hint that the author would prefer you use it, with no enforcement.\nThe one situation where Option 3 makes sense is as a migration step. If you already have a constant that other code references, you can add the class method, update consumers to use it, and then make the constant private or remove it. But that\u0026rsquo;s a transition state, not a destination.\nWhat about Ruby\u0026rsquo;s private_constant? # Ruby does offer Module#private_constant, which restricts access to a constant from outside the class. Used with Option 1, it gets you to roughly the same outcome as Option 2. You\u0026rsquo;d still need a public method for consumers to access the value, which brings you back to the class method pattern anyway.\nIt\u0026rsquo;s a valid tool. I\u0026rsquo;ve been writing Ruby for well over a decade, and I can count on one hand the number of times I\u0026rsquo;ve seen private_constant used in production code. Class methods are idiomatic. They\u0026rsquo;re what other Ruby developers expect. When you\u0026rsquo;re making a design choice, going with the grain of the language and its community matters.\nThe real point # This isn\u0026rsquo;t really about constants vs. methods. It\u0026rsquo;s about whether your classes have intentional APIs or accidental ones.\nEvery time you make a constant public (which is the default), you\u0026rsquo;re implicitly promising that its structure will never change. That\u0026rsquo;s a strong commitment to make accidentally. Class methods let you be deliberate about what you expose and give you room to evolve your implementation without breaking the rest of the system.\nThe cost of switching is close to zero. The cost of not switching shows up later, when refactoring is slow and every change has a blast radius you didn\u0026rsquo;t expect. In my experience leading teams through large codebases, the teams that treat internal API boundaries seriously are the ones that ship faster over time. The shortcuts compound in the wrong direction.\n","date":"19 November 2020","externalUrl":null,"permalink":"/posts/ruby-class-methods-are-better-than-constants/","section":"Posts","summary":"Constants feel clean until six files break when you rename a hash key. Class methods give you the same performance with an API boundary that survives refactoring.","title":"Ruby Constants vs Class Methods: Why Methods are the Better Default","type":"posts"},{"content":"","date":"18 June 2019","externalUrl":null,"permalink":"/tags/elixir/","section":"Tags","summary":"","title":"Elixir","type":"tags"},{"content":"","date":"18 June 2019","externalUrl":null,"permalink":"/tags/phoenix/","section":"Tags","summary":"","title":"Phoenix","type":"tags"},{"content":"Deep linking is one of those features that looks trivial in a sprint planning meeting and then quietly burns a day across three different teams.\nThe concept is simple: a user taps a link, and instead of opening a browser, it opens your native app directly to the right screen. iOS calls these Universal Links. Android calls them App Links. Both require your server to host a verification manifest at a well-known URL so the operating system can confirm your app is authorized to handle links for your domain.\nThe server-side piece is the easiest part of the whole deep linking chain, but it still has a couple of non-obvious gotchas that will cost you debugging time if you don\u0026rsquo;t know about them. This post covers how to set it up in Phoenix so your mobile teams can focus on the client-side integration without waiting on you.\nWhy this breaks across teams # Deep linking requires coordination between three surfaces: the iOS app, the Android app, and the server. Each platform has its own manifest format, its own validation rules, and its own way of failing silently when something is wrong.\nThe server team usually gets pulled in last, right when someone on the mobile side says \u0026ldquo;it\u0026rsquo;s not working and we think it\u0026rsquo;s a server issue.\u0026rdquo; Having the manifests set up correctly from the start, with validation confirmed, removes the server from the debugging chain entirely. That\u0026rsquo;s the goal here.\nCreate the manifest files # Both iOS and Android expect their manifests to be served from a /.well-known/ path on your domain. Create a directory for them in your Phoenix project:\nmkdir -p priv/static/well-known I name this well-known (without the leading dot) so the directory isn\u0026rsquo;t hidden when browsing the project source. The leading dot in the URL path gets handled by the Plug configuration below.\niOS: apple-app-site-association # Create priv/static/well-known/apple-app-site-association (no file extension):\n{ \u0026#34;applinks\u0026#34;: { \u0026#34;apps\u0026#34;: [], \u0026#34;details\u0026#34;: [ { \u0026#34;appID\u0026#34;: \u0026#34;TEAM_ID.com.example.myapp\u0026#34;, \u0026#34;paths\u0026#34;: [\u0026#34;/r/*\u0026#34;] }, { \u0026#34;appID\u0026#34;: \u0026#34;TEAM_ID.com.example.myapp-dev\u0026#34;, \u0026#34;paths\u0026#34;: [\u0026#34;/r/*\u0026#34;] } ] } } Replace TEAM_ID with your Apple Developer Team ID and update the bundle identifiers and paths to match your app. The paths array controls which URL patterns trigger the app to open. Here I\u0026rsquo;m using /r/* as a prefix for redirect-style deep links, but yours will depend on your routing scheme.\nIncluding a -dev entry lets your development builds handle deep links too, which saves your iOS team from constantly switching between builds during testing.\nAndroid: assetlinks.json # Create priv/static/well-known/assetlinks.json:\n[ { \u0026#34;relation\u0026#34;: [\u0026#34;delegate_permission/common.handle_all_urls\u0026#34;], \u0026#34;target\u0026#34;: { \u0026#34;namespace\u0026#34;: \u0026#34;android_app\u0026#34;, \u0026#34;package_name\u0026#34;: \u0026#34;com.example.myapp\u0026#34;, \u0026#34;sha256_cert_fingerprints\u0026#34;: [ \u0026#34;14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5\u0026#34; ] } } ] Replace the package_name and sha256_cert_fingerprints with your app\u0026rsquo;s values. You can get the fingerprint from your signing keystore using keytool -list -v -keystore your-keystore.jks.\nDon\u0026rsquo;t forget to commit # By default, Phoenix\u0026rsquo;s .gitignore excludes priv/static. You\u0026rsquo;ll need to force-add this directory:\ngit add -f priv/static/well-known Alternatively, put the files in a different directory outside priv/static and adjust the Plug configuration below. Either works.\nServe the manifests with Plug.Static # Add a second Plug.Static call in your endpoint, below the existing one:\n# lib/my_app_web/endpoint.ex plug Plug.Static, at: \u0026#34;/.well-known\u0026#34;, from: {:my_app, \u0026#34;priv/static/well-known\u0026#34;}, gzip: false, content_types: %{\u0026#34;apple-app-site-association\u0026#34; =\u0026gt; \u0026#34;application/json\u0026#34;} There are two things worth noting here.\nThe content_types map is not optional for iOS. Without it, Phoenix serves the apple-app-site-association file as text/plain because it has no file extension. iOS requires the Content-Type to be application/json or it silently ignores the file. This is the gotcha that will cost you an afternoon if you don\u0026rsquo;t know about it. Android\u0026rsquo;s assetlinks.json has a .json extension, so Plug handles its content type automatically.\nThe at: \u0026quot;/.well-known\u0026quot; path maps to the well-known directory (no dot) on disk. This is why we named the directory without the leading dot; it keeps the files visible in your project while serving them at the correct URL path.\nVerify before you ship # Don\u0026rsquo;t wait for your mobile team to tell you something is broken. Validate both manifests before you merge:\nAndroid: Google\u0026rsquo;s Digital Asset Links tool checks that assetlinks.json is served correctly and that the fingerprint matches. iOS: Branch\u0026rsquo;s AASA Validator confirms that apple-app-site-association is served with the right content type and valid JSON structure. Run both validators against your staging environment after deploy. They catch the silent failures (wrong content type, malformed JSON, unreachable URL) that neither your server logs nor your mobile team\u0026rsquo;s debugger will surface clearly.\nThe coordination takeaway # The technical setup here is straightforward. The reason deep linking burns time isn\u0026rsquo;t complexity; it\u0026rsquo;s that the failure modes are silent and the ownership is split across teams. When the server manifest is wrong, iOS just doesn\u0026rsquo;t open the app. No error. No log. The mobile developer files a bug, the server developer says \u0026ldquo;the file is there,\u0026rdquo; and everyone spends an afternoon before someone checks the Content-Type header.\nSet this up once, validate it with the tools above, and your side of the deep linking chain is solid. Your mobile teams can debug their integration knowing the server isn\u0026rsquo;t the variable.\n","date":"18 June 2019","externalUrl":null,"permalink":"/posts/serving-universal-links-app-links-from-phoenix/","section":"Posts","summary":"Deep linking looks simple until it breaks in production because three teams own different pieces of it. Here’s the Phoenix server-side setup, including the content-type gotcha that will cost you an afternoon.","title":"Serving iOS Universal Links and Android App Links from Phoenix","type":"posts"},{"content":"The fastest way to slow down a team is to let small annoyances pile up in the local dev environment.\nNobody notices one developer copy-pasting an ngrok hostname into a config file. But multiply that by every engineer on the team, several times a day, across weeks, and you\u0026rsquo;ve got a real drag on iteration speed. Worse, it trains people to accept friction as normal. Once a team stops noticing the small stuff, they stop fixing the medium stuff too.\nThis post solves a specific problem (dynamically wiring an ngrok hostname into a Phoenix dev server), but the principle is bigger: every manual step in your dev workflow is a candidate for automation, and leaders who treat the dev environment as a product ship faster.\nThe problem # ngrok is the standard tool for exposing a local server to the internet. You need it for mobile testing, webhook development, showing work to a stakeholder on another network, and plenty more.\nOn ngrok\u0026rsquo;s free tier, you get a randomly generated hostname every time you start a tunnel. That means you need to copy the new hostname, paste it into your Phoenix config or environment, and restart the server. Every single time.\nIt\u0026rsquo;s a small thing. It\u0026rsquo;s also the kind of small thing that interrupts flow state, and flow state is where the actual work happens.\nUpdate (2024): ngrok now offers free static domains on their free tier, which eliminates this problem entirely for most use cases. If you\u0026rsquo;re starting fresh, use a static domain and skip the automation below. The rest of this post is still useful if you\u0026rsquo;re on a plan without static domains, or if you want to see how the automation pattern works for other dynamic-config problems.\nConfigure Phoenix to use a dynamic host # Set up your config/dev.exs to read the hostname from an environment variable:\n# config/dev.exs config :my_app, MyApp.Endpoint, http: [port: 4000], url: [scheme: \u0026#34;https\u0026#34;, host: {:system, \u0026#34;HOST\u0026#34;}, port: \u0026#34;443\u0026#34;], debug_errors: true, code_reloader: true, check_origin: false, watchers: [ node: [ \u0026#34;node_modules/webpack/bin/webpack.js\u0026#34;, \u0026#34;--mode\u0026#34;, \u0026#34;development\u0026#34;, \u0026#34;--watch-stdin\u0026#34;, cd: Path.expand(\u0026#34;../assets\u0026#34;, __DIR__) ] ] This tells Phoenix to generate URLs using whatever HOST is set to at boot time, with HTTPS on port 443 (which is what ngrok provides). If you need plain HTTP instead, change the scheme to \u0026quot;http\u0026quot; and the port to \u0026quot;80\u0026quot;.\nThe key detail: {:system, \u0026quot;HOST\u0026quot;} reads the value at runtime, not compile time. That\u0026rsquo;s what makes this work with a hostname that changes on every ngrok restart.\nAuto-populate the hostname with a Makefile # ngrok runs a local API at http://127.0.0.1:4040 that exposes tunnel metadata. We can query it to grab the current public hostname and inject it into Phoenix automatically.\nThis requires curl and jq to be installed (both are common on macOS and Linux).\nNGROK_HOST := $$(curl --silent http://127.0.0.1:4040/api/tunnels \\ | jq \u0026#39;.tunnels[0].public_url\u0026#39; \\ | tr -d \u0026#39;\u0026#34;\u0026#39; \\ | awk -F/ \u0026#39;{print $$3}\u0026#39;) .PHONY: serve-ngrok serve-ngrok: ## Start Phoenix bound to the active ngrok tunnel @echo \u0026#34;🌍 Exposing My App @ https://$(NGROK_HOST)\u0026#34; env HOST=$(NGROK_HOST) mix phx.server Here\u0026rsquo;s what\u0026rsquo;s happening: curl hits the ngrok API, jq extracts the tunnel URL, and awk strips it down to just the hostname. That hostname gets passed as the HOST environment variable when Phoenix boots. Zero copy-pasting.\nIf you prefer a bash script over Make, the same curl | jq | awk pipeline works anywhere.\nThe workflow # Two terminals, two commands:\nngrok http 4000 in one shell to start the tunnel make serve-ngrok in another to boot Phoenix with the correct hostname already wired in That\u0026rsquo;s it. The hostname is resolved at boot, Phoenix generates correct URLs, and you never touch a config file.\nThe bigger point # This is a five-minute automation. That\u0026rsquo;s exactly why it matters.\nMost dev-environment friction doesn\u0026rsquo;t come from one big broken thing. It comes from dozens of tiny annoyances that nobody fixes because each one feels too small to bother with. Copy-paste a hostname here, restart a service there, manually seed a database, toggle a feature flag by hand.\nIndividually, none of them are worth a meeting. Collectively, they\u0026rsquo;re the reason your team\u0026rsquo;s local setup takes a full day for new hires and why half the engineers have slightly different workarounds for the same problems.\nIf you lead a team, audit the manual steps in your dev workflow the same way you\u0026rsquo;d audit tech debt in production. The fix is usually simple. The compound effect of not fixing it is not.\n","date":"13 June 2019","externalUrl":null,"permalink":"/posts/expose-phoenix-app-via-free-ngrok/","section":"Posts","summary":"A Makefile trick that auto-populates the ngrok hostname into your Phoenix dev server. Small dev-environment friction compounds across a team faster than you’d expect.","title":"Expose Your Phoenix App via ngrok (and Why Dev Friction Compounds)","type":"posts"},{"content":"Switching from a traditional in-office job to a remote job puts your habits and skills to the test. The positive habits you developed in an office often produce negative results in a remote setting.\n\u0026ldquo;Just get it done\u0026rdquo; \u0026ldquo;I want to see results\u0026rdquo; \u0026ldquo;Can you help me sometime later today?\u0026rdquo; \u0026ldquo;I\u0026rsquo;ll grab you when I can\u0026rdquo; Each of these things have been often encouraged by prior employers as ways to get ahead and be an effective team member. However since going remote five years ago, each of these habits has gone through a systematic process to undo them.\nJust get it done # My fictional boss Stephen comes in with his morning coffee and on his way to his office says, \u0026ldquo;I have a project I need done, meet me in my office in five minutes.\u0026rdquo; I grab my notebook and head into his office to find out more. The next thirty minutes are spent covering a new initiative that needs to be addressed by next week. If I am to do it right I know I better be thorough before stepping out of the meeting less I forget an important detail.\nStarting out my career as a junior developer I yearned for the opportunity to do the \u0026lsquo;fun pet projects\u0026rsquo; that my senior peers worked on. I\u0026rsquo;d pressure my boss with a \u0026lsquo;why not me?\u0026rsquo; argument whenever the opportunity presented itself. It took me a few years to realize what made my co-workers \u0026lsquo;pet project worthy\u0026rsquo; was they \u0026ldquo;just got it done\u0026rdquo;.\nTaking on this new \u0026ldquo;just get it done\u0026rdquo; stance at the office worked out great. I quickly became the go-to employee for the majority of projects. However, when I switched to a remote position, this way of working became detrimental to my career and success at the company.\nWhere is Justin? Let\u0026rsquo;s go over to his desk. # There is a major difference between being in an office environment and a remote environment: PEOPLE CAN SEE YOU. I am aware I just stated the obvious here, but let that sink in for a minute.\nThe \u0026ldquo;just get it done\u0026rdquo; approach exchanges direct communication for observational communication. What I mean by this is instead of me nagging my boss for clarifications and additional requirements, we instead communicated progress by observing: staying late, being the first one in, and having working lunches. He knew, or more accurately he assumed, that every step of the way I was busting my ass to get the project done. He didn\u0026rsquo;t come ask me, he judged by what he saw. This observational communication built a level of trust between us that was pivotal for the whole system to work.\nNow I know that my boss seeing me in my seat doesn\u0026rsquo;t mean that I\u0026rsquo;m going to make the deadline or that I\u0026rsquo;m not struggling, that is not the point. The fact of the matter is, this is how it worked. He was reading my body language and used that to reaffirm his assumptions. It is not ideal, but it\u0026rsquo;s the reality.\nIs Justin alive? He hasn\u0026rsquo;t posted in chat. # In a remote environment body language goes out the window as it\u0026rsquo;s not possible to see. Even when on video you often cannot trust it due to latency or other technical delays. This means all of the observational communication mechanisms we used in-office no longer function correctly. It is worth noting though that our habits can drive us to manifest observational results even in a remote environment.\nWhat I mean by manifesting results is we look at organizational \u0026lsquo;water-coolers\u0026rsquo; as a stand-in for the in-office desk. If I need a status update on a project I instinctively go look at my emails, chat rooms, and even Github repos for signs of life. This is my virtual way of searching the office for the person who isn\u0026rsquo;t at their desk.\nRegardless of environment, I can tell you from experience there is no better way to lose trust in someone when you need answers to something and they are no where to be found (regardless of environment)\nReplacing \u0026ldquo;just get it done\u0026rdquo; with \u0026ldquo;notify and handle it\u0026rdquo; # Folks working in a remote environment have a shared responsibility to each other. They must \u0026ldquo;fill the void\u0026rdquo; left by the lack of behavioral and observational communication with written communication. This is why I suggest folks working in this setting replace the \u0026ldquo;just get it done\u0026rdquo; approach with a \u0026ldquo;notify and handle it\u0026rdquo; approach.\nThe adjustment that needs to be made for the \u0026ldquo;notify and handle it\u0026rdquo; approach is actively communicating out the events come up and handling them accordingly.\nNotification # To clarify what I mean by an event, I am talking about anything of interest, planned or unplanned, setbacks and milestones. Actively communicating out these sorts of things builds trust all around. It instills a sense of trust that nothing is being missed or fallen through the cracks.\nHandle it # The \u0026ldquo;just get it done\u0026rdquo; approach follows the idea that when you are assigned something you do whatever it takes to get it done. This part goes unchanged when remote. The difference is really in the notification step it\u0026rsquo;s paired with. If something unexpected happens I will figure out my options and come up with a recommended solution to the issue. From there I\u0026rsquo;ll quickly pop open my email and kick off an email to the invested parties:\nTeam, I ran into an unexpected issue with firewall rules one of our web nodes that will prevent us from using web sockets safely. I\u0026rsquo;ve looked at our options and suggest that we set up some boxes in a DMZ zone to bypass this issue and still maintaining security of the existing servers. - Please let me know if there is any issue with this approach or if you want more detail. I\u0026rsquo;d be happy to set something up.\nI\u0026rsquo;d usually at this point give it an hour before I jumped on doing the solution just in case I misunderstood. Otherwise I\u0026rsquo;m off to implement what I mentioned above.\nThis isn\u0026rsquo;t an exact science, it\u0026rsquo;s understanding that when remote you must keep folks abreast as to what you\u0026rsquo;re working on. Without it people begin to wonder if you are stuck or heading off track. These quick emails serve as a reminder that you are neither of those things.\n","date":"1 March 2019","externalUrl":null,"permalink":"/posts/going-remote/","section":"Posts","summary":"The habits that made you successful in an office will hurt you when you go remote. Here’s how to replace observational communication with written communication.","title":"Going Remote","type":"posts"},{"content":"Shared partials are where Rails views go to rot.\nEvery partial starts clean: extract some duplicated HTML, render it from a few places, move on. Then someone adds a local variable. Then a conditional. Then every caller needs to pass blog_uri: nil just to avoid a NameError, and suddenly your \u0026ldquo;simple\u0026rdquo; partial is a maintenance burden that confuses every new developer who reads it.\nRails has had a built-in solution for this since before most of us started using the framework. It\u0026rsquo;s called local_assigns, and it\u0026rsquo;s so old that even DHH forgot it existed. This post walks through how partials typically degrade and how local_assigns keeps them clean.\nA quick note: the examples here use partials with local variables, not instance variables. Local variables are the better default for partials because they make the data dependency explicit at the call site. That\u0026rsquo;s a separate argument, but it\u0026rsquo;s why you\u0026rsquo;ll see render 'footer', blog_uri: ... instead of @blog_uri throughout.\nStage 1: The clean extraction # You have a footer that shows up on every page. Privacy policy, terms of service, copyright. You extract it into a partial:\n\u0026lt;%= render \u0026#39;footer\u0026#39; %\u0026gt; \u0026lt;footer\u0026gt; \u0026lt;ul class=\u0026#34;inline\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Privacy Policy\u0026#39;, privacy_policy_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Terms of Service\u0026#39;, terms_of_service_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;p\u0026gt;\u0026amp;copy; 2017 Overstuffed Gorilla LLC\u0026lt;/p\u0026gt; \u0026lt;/footer\u0026gt; No logic, no variables, no problems. This is the honeymoon phase.\nStage 2: Add some dynamic data # A feature request comes in: show the user\u0026rsquo;s blog and Facebook links above the company footer on logged-in pages. You add two local variables:\n\u0026lt;%= render \u0026#39;footer\u0026#39;, blog_uri: @current_user.blog_uri, facebook_uri: @current_user.facebook_uri %\u0026gt; \u0026lt;footer\u0026gt; \u0026lt;section class=\u0026#34;user-links\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;inline\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Blog\u0026#39;, blog_uri %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Facebook\u0026#39;, facebook_uri %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;section class=\u0026#34;company-links\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;inline\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Privacy Policy\u0026#39;, privacy_policy_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Terms of Service\u0026#39;, terms_of_service_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;p\u0026gt;\u0026amp;copy; 2017 Overstuffed Gorilla LLC\u0026lt;/p\u0026gt; \u0026lt;/footer\u0026gt; This works on logged-in pages. But you\u0026rsquo;ve made a big assumption: every page that renders this partial now needs to provide those two variables.\nStage 3: It breaks # The login page, the 404 page, the marketing site; none of them have a current_user. They all render the footer. They all blow up with NameError: undefined local variable or method 'blog_uri'.\nYour first instinct is to add a conditional inside the partial:\n\u0026lt;% if blog_uri \u0026amp;\u0026amp; facebook_uri %\u0026gt; \u0026lt;section class=\u0026#34;user-links\u0026#34;\u0026gt; ... \u0026lt;/section\u0026gt; \u0026lt;% end %\u0026gt; This looks right but doesn\u0026rsquo;t work. The if statement still evaluates blog_uri, which triggers the same NameError when the variable was never passed. Ruby doesn\u0026rsquo;t know the variable exists at all; it\u0026rsquo;s not nil, it\u0026rsquo;s undefined.\nThe anti-pattern: nil guards everywhere # The most common fix I\u0026rsquo;ve seen in the wild is to pass explicit nils from every caller that doesn\u0026rsquo;t have the data:\n\u0026lt;%= render \u0026#39;footer\u0026#39;, blog_uri: nil, facebook_uri: nil %\u0026gt; Or worse, with inline conditionals:\n\u0026lt;%= render \u0026#39;footer\u0026#39;, blog_uri: (@current_user ? @current_user.blog_uri : nil), facebook_uri: (@current_user ? @current_user.facebook_uri : nil) %\u0026gt; This eliminates the error, but it creates a different problem. Every caller now has to mention variables that have no meaning in its context. A developer reading the login page template sees blog_uri: nil and reasonably asks: \u0026ldquo;Why would there ever be a blog URI on the login page? Is this dead code? Should I remove it?\u0026rdquo;\nThat confusion multiplies with every optional variable you add. Three optional variables means every caller needs three nil assignments. Five variables, five assignments. The partial was supposed to reduce duplication, and now it\u0026rsquo;s creating a different kind of it.\nThe fix: local_assigns # local_assigns is a hash that Rails makes available inside every partial. It contains only the variables that were actually passed to that render call. If a variable wasn\u0026rsquo;t passed, it\u0026rsquo;s simply not in the hash. No NameError, no nil guards.\n\u0026lt;%# Logged-in pages pass the user links %\u0026gt; \u0026lt;%= render \u0026#39;footer\u0026#39;, blog_uri: @current_user.blog_uri, facebook_uri: @current_user.facebook_uri %\u0026gt; \u0026lt;%# Everything else just renders the footer %\u0026gt; \u0026lt;%= render \u0026#39;footer\u0026#39; %\u0026gt; \u0026lt;footer\u0026gt; \u0026lt;% if local_assigns[:blog_uri] \u0026amp;\u0026amp; local_assigns[:facebook_uri] %\u0026gt; \u0026lt;section class=\u0026#34;user-links\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;inline\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Blog\u0026#39;, local_assigns[:blog_uri] %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Facebook\u0026#39;, local_assigns[:facebook_uri] %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;% end %\u0026gt; \u0026lt;section class=\u0026#34;company-links\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;inline\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Privacy Policy\u0026#39;, privacy_policy_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;%= link_to \u0026#39;Terms of Service\u0026#39;, terms_of_service_url %\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;p\u0026gt;\u0026amp;copy; 2017 Overstuffed Gorilla LLC\u0026lt;/p\u0026gt; \u0026lt;/footer\u0026gt; The partial handles both cases cleanly. Callers that have the data pass it. Callers that don\u0026rsquo;t just skip it. No nil guards, no confusion, no extra variables cluttering templates where they don\u0026rsquo;t belong.\nWhy this matters beyond the syntax # Partial complexity is a leading indicator of view-layer health. When you see partials accumulating boolean flags, nil guards, and mode switches, it usually means the abstraction boundary is in the wrong place or the interface wasn\u0026rsquo;t designed to handle optional behavior.\nlocal_assigns is a small tool, but it solves the right problem: it lets a partial have a flexible interface without pushing that flexibility cost onto every caller. The same principle applies everywhere in software design. The component that offers optional behavior should handle the optionality internally, not force every consumer to manage it.\nIn my experience, view code is the most likely place for this kind of accidental complexity to accumulate unchecked. Backend code gets reviewed for design. Views get reviewed for \u0026ldquo;does it look right.\u0026rdquo; Patterns like local_assigns are how you keep the view layer from quietly becoming the most expensive part of your codebase to change.\n","date":"14 July 2017","externalUrl":null,"permalink":"/posts/extending-rails-partials-using-local_assigns/","section":"Posts","summary":"Rails partials rot fast once multiple views depend on them. local_assigns is a forgotten built-in that handles optional variables without the nil-guard sprawl.","title":"Extending Rails Partials with local_assigns (and Avoiding the Nil-Guard Trap)","type":"posts"},{"content":"","date":"14 July 2017","externalUrl":null,"permalink":"/tags/rails/","section":"Tags","summary":"","title":"Rails","type":"tags"},{"content":" Historical post (2016). Elm has largely faded from mainstream use, and Phoenix LiveView has replaced the need for this kind of server-side rendering setup. This post is preserved as a companion to the React with Phoenix experiment. In my last post @mjackson and I got server side rendering of React setup. After that I was wondering if I could do something similar with Elm. I\u0026rsquo;ve played with Elm but not much so for this exercise I just grabbed the Todo app\nBefore we proceed # I\u0026rsquo;m going to use a very similar setup to the React server side rendering. One thing to note is that I found a small bug with min-document. At the time of writing there is a bug where it doesn\u0026rsquo;t handle boolean html attributes correctly. I\u0026rsquo;ve added a PR to get this sorted, but for now we\u0026rsquo;ll have to overwrite that dependency.\nThis uses the same method as the React STDIO method. We run a collection of IO servers that take a JSON blob and spit out JSON back to us with the rendered HTML.\nYou\u0026rsquo;ll need to install Elm. I use brew\nbrew install elm See the demo app # If you want to just see the demo application you can find it at hassox/phoenix_elm.\nServer code # Generate a new application and follow getting webpack in place at http://matthewlehner.net/using-webpack-with-phoenix-and-elixir/. We\u0026rsquo;re going to use it to generate our app.js bundle, and also our server bundle.\nTo generate our application bundle we\u0026rsquo;ll use a pretty standard setup for Elm. Here\u0026rsquo;s the webpack.config.js\n// setup from http://matthewlehner.net/using-webpack-with-phoenix-and-elixir/ var ExtractTextPlugin = require(\u0026#34;extract-text-webpack-plugin\u0026#34;); var CopyWebpackPlugin = require(\u0026#34;copy-webpack-plugin\u0026#34;); module.exports = { entry: [\u0026#34;./web/static/js/app.js\u0026#34;, \u0026#34;./web/static/css/app.css\u0026#34;], output: { path: \u0026#34;./priv/static/\u0026#34;, filename: \u0026#34;js/app.js\u0026#34; }, module: { noParse: /\\.elm$/, loaders: [ { test: /\\.js$/, exclude: /node_modules/, loader: \u0026#34;babel\u0026#34;, query: { presets: [\u0026#34;es2015\u0026#34;] } }, { test: /\\.elm$/, exclude: [/elm-stuff/, /node_modules/], loader: \u0026#39;elm-webpack\u0026#39; }, { test: /\\.css$/, loader: ExtractTextPlugin.extract(\u0026#34;css\u0026#34;) }, { test: /\\.(png|woff|woff2|eot|ttf|svg)$/, loader: \u0026#39;url-loader?limit=100000\u0026#39; }, ] }, resolve: { alias: { phoenix_html: __dirname + \u0026#34;/deps/phoenix_html/web/static/js/phoenix_html.js\u0026#34;, phoenix: __dirname + \u0026#34;./deps/phoenix/web/static/js/phoenix.js\u0026#34; } }, plugins: [ new ExtractTextPlugin(\u0026#34;css/app.css\u0026#34;), new CopyWebpackPlugin([{ from: \u0026#34;./web/static/assets\u0026#34; }]) ], devServer: { inline: true, stats: \u0026#39;errors-only\u0026#39; } }; We\u0026rsquo;ve included an Elm Loader. Just for reference here\u0026rsquo;s my package.json file:\n{ \u0026#34;repository\u0026#34;: {}, \u0026#34;dependencies\u0026#34;: {}, \u0026#34;devDependencies\u0026#34;: { \u0026#34;babel-core\u0026#34;: \u0026#34;^6.3.26\u0026#34;, \u0026#34;babel-loader\u0026#34;: \u0026#34;^6.2.0\u0026#34;, \u0026#34;babel-preset-es2015\u0026#34;: \u0026#34;^6.3.13\u0026#34;, \u0026#34;copy-webpack-plugin\u0026#34;: \u0026#34;^0.3.3\u0026#34;, \u0026#34;css-loader\u0026#34;: \u0026#34;^0.23.1\u0026#34;, \u0026#34;elm-stdio\u0026#34;: \u0026#34;^1.0.1\u0026#34;, \u0026#34;elm-webpack-loader\u0026#34;: \u0026#34;^1.1.1\u0026#34;, \u0026#34;extract-text-webpack-plugin\u0026#34;: \u0026#34;^0.9.1\u0026#34;, \u0026#34;file-loader\u0026#34;: \u0026#34;^0.8.5\u0026#34;, \u0026#34;min-document\u0026#34;: \u0026#34;hassox/min-document\u0026#34;, \u0026#34;style-loader\u0026#34;: \u0026#34;^0.13.0\u0026#34;, \u0026#34;url-loader\u0026#34;: \u0026#34;^0.5.7\u0026#34;, \u0026#34;virtual-dom\u0026#34;: \u0026#34;^2.1.1\u0026#34;, \u0026#34;webpack\u0026#34;: \u0026#34;^1.12.9\u0026#34; }, \u0026#34;scripts\u0026#34;: { \u0026#34;compile\u0026#34;: \u0026#34;webpack -p\u0026#34; } } I mostly copied the react-stdio js module and tweaked it for Elm.\nOk. So this should be working. Lets get the Todo Elm code and put it in web/static/elm. Grab it from web/static/elm/Todo.elm. You\u0026rsquo;ll also need to replace the web/static/css/app.css.\nNote I had to tweak the css a little to remove the background image. Something isn\u0026rsquo;t quite right in the webpack config.\nAt this point we need to make sure we have our elm dependencies installed.\nelm-package install evancz/elm-html Ok, so now we have our Elm file. Lets make it work. In you web/static/js/app.js file render out your Elm application.\nimport \u0026#34;phoenix_html\u0026#34; import Elm from \u0026#39;../elm/Todo.elm\u0026#39; Elm.embed(Elm.Todo, document.getElementById(\u0026#39;elm-main\u0026#39;), {getStorage: null}); And lets tweak the layout web/templates/layout/app.html.eex\n\u0026lt;body\u0026gt; \u0026lt;div id=\u0026#39;elm-main\u0026#39;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script src=\u0026#34;\u0026lt;%= static_path(@conn, \u0026#34;/js/app.js\u0026#34;) %\u0026gt;\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; If you reload your page you should be see the application. Ok real progress. Now lets get our server bundle ready to communicate with the elm-stdio process.\nWe\u0026rsquo;ll add another webpack config to build our server bundle. It looks like:\nmodule.exports = { entry: \u0026#34;./web/static/js/server_render.js\u0026#34;, output: { path: \u0026#34;./priv/server\u0026#34;, filename: \u0026#34;js/main.js\u0026#34;, library: \u0026#34;myComponent\u0026#34;, libraryTarget: \u0026#34;commonjs2\u0026#34; }, module: { noParse: /\\.elm$/, loaders: [ { test: /\\.js$/, exclude: /node_modules/, loader: \u0026#34;babel\u0026#34;, query: { presets: [\u0026#34;es2015\u0026#34;] } }, { test: /\\.elm$/, exclude: [/elm-stuff/, /node_modules/], loader: \u0026#39;elm-webpack\u0026#39; }, ] }, resolve: { alias: { phoenix_html: __dirname + \u0026#34;/deps/phoenix_html/web/static/js/phoenix_html.js\u0026#34;, phoenix: __dirname + \u0026#34;./deps/phoenix/web/static/js/phoenix.js\u0026#34; } } }; Note that we\u0026rsquo;re telling it to output a commonjs2 library component! (That\u0026rsquo;s important).\nAdd a watcher in your config/dev.exs to pick up changes. According to our webpack server config we\u0026rsquo;re looking for a file at web/static/js/server_render.js so lets make that.\nexport default require(\u0026#39;../elm/Todo.elm\u0026#39;); That\u0026rsquo;s it. That will export Elm for us with the Todo app.\nAdd a watcher to pick up the changes:\nwatchers: [ {\u0026#34;node\u0026#34;, [\u0026#34;node_modules/webpack/bin/webpack.js\u0026#34;, \u0026#34;--watch-stdin\u0026#34;, \u0026#34;--progress\u0026#34;, \u0026#34;--colors\u0026#34;]}, {\u0026#34;node\u0026#34;, [\u0026#34;node_modules/webpack/bin/webpack.js\u0026#34;, \u0026#34;--watch-stdin\u0026#34;, \u0026#34;--progress\u0026#34;, \u0026#34;--colors\u0026#34;, \u0026#34;--config\u0026#34;, \u0026#34;webpack.server.config.js\u0026#34;]}, ] If you restart your server now you should see two bundles built. You should see a file in priv/server/js/main.js\nPhew. Almost there.\nStart your IO servers # Lets add the IO servers now.\nInstall std_json_io into your Phoenix application.\nmix.exs\ndef deps do [#snip {:std_json_io, \u0026#34;~\u0026gt; 0.1.0\u0026#34;}, ] end def applications do [applications: [...., :std_json_io]] end Create the supervisor lib/phoenix_elm/elm_io.ex\ndefmodule PhoenixElm.ElmIo do use StdJsonIo, otp_app: :phoenix_elm, script: \u0026#34;elm-stdio\u0026#34; end And add it to your supervisor tree in your app file lib/phoenix_elm.ex\ndef start(_type, _args) do #snip children = [ #snip supervisor(PhoenixElm.ElmIo, []), ] opts = [strategy: :one_for_one, name: PhoenixElm.Supervisor] Supervisor.start_link(children, opts) end One more thing, we\u0026rsquo;ll watch the generated server bundle for changes in dev so it reloads. config/dev.exs\nconfig :phoenix_elm, PhoenixElm.ElmIo, watch_files: [Path.join(__DIR__, \u0026#34;../priv/server/js/main.js\u0026#34;) |\u0026gt; Path.expand] Ok nearly done. We need to now make our call. We\u0026rsquo;ll just use the PageController. Update the routes to forward to it.\nforward \u0026#34;/\u0026#34;, PageController, :index Update the PageView file to add a render method.\ndefmodule PhoenixElm.PageView do use PhoenixElm.Web, :view def render(\u0026#34;index.html\u0026#34;, options) do data = %{ getStorage: nil } opts = %{ path: \u0026#34;./priv/server/js/main.js\u0026#34;, component: \u0026#34;Todo\u0026#34;, render: \u0026#34;embed\u0026#34;, id: \u0026#34;elm-main\u0026#34;, data: data, } result = PhoenixElm.ElmIo.json_call!(opts) render \u0026#34;single_page.html\u0026#34;, html: result[\u0026#34;html\u0026#34;], data: data end end Note that we\u0026rsquo;re using the embed method. I haven\u0026rsquo;t got to the bottom of it yet, but the fullscreen method doesn\u0026rsquo;t wire up your listeners properly. The embed seems to work just fine though.\nOk so nearly there. We need to create our single_page.html template, and update our layout and app.js file.\nsingle_page.html\n\u0026lt;%= raw @html %\u0026gt; \u0026lt;script\u0026gt;window.APP_DATA = \u0026lt;%= raw Poison.encode!(@data) %\u0026gt;;\u0026lt;/script\u0026gt; web/templates/layout/app.html.eex\n\u0026lt;body\u0026gt; \u0026lt;%= render @view_module, @view_template, assigns %\u0026gt; \u0026lt;script src=\u0026#34;\u0026lt;%= static_path(@conn, \u0026#34;/js/app.js\u0026#34;) %\u0026gt;\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; And finally our app.js\nimport \u0026#34;phoenix_html\u0026#34; import Elm from \u0026#39;../elm/Todo.elm\u0026#39; Elm.embed(Elm.Todo, document.getElementById(\u0026#39;elm-main\u0026#39;), APP_DATA); In the template we splatted the params as APP_DATA. Ok so now if you restart the server and load the page, you should have server rendered Elm!\n","date":"4 January 2016","externalUrl":null,"permalink":"/posts/server-side-elm-with-phoenix/","section":"Posts","summary":"Server-side render Elm applications with Phoenix using a STDIO-based IO server, similar to the React approach.","title":"Server side Elm with Phoenix","type":"posts"},{"content":" Historical post (2016). The approach described here uses react-stdio, which is no longer maintained. Phoenix LiveView has since become the standard for server-rendered interactive UIs in the Elixir ecosystem. This post is preserved as a snapshot of early experiments in server-side rendering with Phoenix. Over the holiday period this year I was lucky enough to stay in Tahoe for a week. The kids love a white Christmas. While I was there, @mjackson and his family came up for a couple days. Sledding during the day, and coding at night, what started as an impromptu Phoenix tutorial quickly became an idea between the two of us for server side rendered React with Phoenix. This post is to show what we came up with.\nIn pretty sort order Michael came up with react-stdio a simple javascript server that will require a file and render it as a React component over STDIO/STDOUT. It took me a little longer to come up with the goods for the Phoenix side, mostly because this was my first real foray into OTP.\nEnough already. How? # Ok, so for those who are impatient, you can see a demo application at hassox/react_phx_stdio. If you want the gory details read on.\nBefore we proceed # I\u0026rsquo;ve only got this working with webpack, so if you want to follow along, you\u0026rsquo;ll need your application to use it. I followed http://matthewlehner.net/using-webpack-with-phoenix-and-elixir/ to get setup (mostly).\nAlso bear in mind that I\u0026rsquo;m a React n00b so don\u0026rsquo;t expect to be wowed on that front.\nThe general overview # So, what we\u0026rsquo;re going to do is use React in our app.js. The main component that we\u0026rsquo;re going to use we\u0026rsquo;ll need to pass to our react-stdio process on the server side. We\u0026rsquo;ll get webpack to generate a bundle of just the component, and compile it to a server location so we can pass it to react-stdio from our view.\nWe\u0026rsquo;re going to start up a pool of react-stdio servers and on each request, we\u0026rsquo;ll pass the component bundle location and the props that the component needs to render it. We\u0026rsquo;ll include the props on the page and use that to render over the top (react will notice and not render stuff).\nThe code # First, lets get the javascript building the server side asset. We\u0026rsquo;ll hook into Phoenix\u0026rsquo;s watchers to run a separate webpack in development.\nServer javascript # We\u0026rsquo;ll get webpack to write a component bundle to a server location to pass to react-stdio.\nThe server side webpack.server.config.js looks like:\nmodule.exports = { entry: { component: \u0026#34;./web/static/js/components/main.js\u0026#34;, }, output: { path: \u0026#34;./priv/server/js\u0026#34;, filename: \u0026#34;component.js\u0026#34;, library: \u0026#34;myComponent\u0026#34;, libraryTarget: \u0026#34;commonjs2\u0026#34; }, module: { loaders: [{ test: /\\.js$/, exclude: /node_modules/, loader: \u0026#34;babel\u0026#34;, query: { presets: [\u0026#34;es2015\u0026#34;, \u0026#34;react\u0026#34;] } }], }, resolve: { alias: { phoenix_html: __dirname + \u0026#34;/deps/phoenix_html/web/static/js/phoenix_html.js\u0026#34;, phoenix: __dirname + \u0026#34;./deps/phoenix/web/static/js/phoenix.js\u0026#34; } } }; We still need to be able to render our component so we need to make sure all the right requires are present. Notice that we told the output to be a commonjs2 library. That\u0026rsquo;s an important step!\nFor reference, my package.json looks like:\n{ \u0026#34;repository\u0026#34;: {}, \u0026#34;dependencies\u0026#34;: {}, \u0026#34;devDependencies\u0026#34;: { \u0026#34;babel-core\u0026#34;: \u0026#34;^6.3.26\u0026#34;, \u0026#34;babel-loader\u0026#34;: \u0026#34;^6.2.0\u0026#34;, \u0026#34;babel-preset-es2015\u0026#34;: \u0026#34;^6.3.13\u0026#34;, \u0026#34;babel-preset-react\u0026#34;: \u0026#34;^6.3.13\u0026#34;, \u0026#34;bootstrap\u0026#34;: \u0026#34;^3.3.6\u0026#34;, \u0026#34;copy-webpack-plugin\u0026#34;: \u0026#34;^0.3.3\u0026#34;, \u0026#34;css-loader\u0026#34;: \u0026#34;^0.23.1\u0026#34;, \u0026#34;extract-text-webpack-plugin\u0026#34;: \u0026#34;^0.9.1\u0026#34;, \u0026#34;file-loader\u0026#34;: \u0026#34;^0.8.5\u0026#34;, \u0026#34;react\u0026#34;: \u0026#34;^0.14.5\u0026#34;, \u0026#34;react-dom\u0026#34;: \u0026#34;^0.14.5\u0026#34;, \u0026#34;react-stdio\u0026#34;: \u0026#34;^2.0.6\u0026#34;, \u0026#34;style-loader\u0026#34;: \u0026#34;^0.13.0\u0026#34;, \u0026#34;url-loader\u0026#34;: \u0026#34;^0.5.7\u0026#34;, \u0026#34;webpack\u0026#34;: \u0026#34;^1.12.9\u0026#34; }, \u0026#34;scripts\u0026#34;: { \u0026#34;compile\u0026#34;: \u0026#34;webpack -p\u0026#34; } } Lets get a simple react component into web/static/js/components/main.js\nimport React from \u0026#39;react\u0026#39; const { string } = React.PropTypes const HelloWorld = React.createClass({ propTypes: { message: string.isRequired }, getDefaultProps() { return { message: \u0026#34;The default message\u0026#34; } }, render() { const { message } = this.props return ( \u0026lt;p\u0026gt;{message}\u0026lt;/p\u0026gt; ) } }) export default HelloWorld Ok so we have our little component ready. We need to get webpack building it into the location where we can give it to react-stdio. In your config/dev.exs we\u0026rsquo;ll add a watcher to compile it.\nwatchers: [ {\u0026#34;node\u0026#34;, [ \u0026#34;node_modules/webpack/bin/webpack.js\u0026#34;, \u0026#34;--watch-stdin\u0026#34;, \u0026#34;--progress\u0026#34;, \u0026#34;--colors\u0026#34; ] }, {\u0026#34;node\u0026#34;, [ \u0026#34;node_modules/webpack/bin/webpack.js\u0026#34;, \u0026#34;--watch-stdin\u0026#34;, \u0026#34;--progress\u0026#34;, \u0026#34;--colors\u0026#34;, \u0026#34;--config\u0026#34;, \u0026#34;webpack.server.config.js\u0026#34; ] }, ] These are almost the same command but one uses webpack.config.js and one has webpack.server.config.js.\nOne thing to note: I had some trouble getting two watchers with the same key when using atoms. Phoenix seemed to collapse the two into one as a Keyword list. By using string keys this collapsing doesn\u0026rsquo;t happen.\nRestart your server and you should see your component.js file build.\nThat\u0026rsquo;s most of the JS part sorted for now. We\u0026rsquo;ll do the server part and then finish wiring it together to get React to hijack the pre-rendered page.\nServer setup # At this point, we need to get std_json_io installed into our application.\ndef application do [applciations: [...., :std_json_io]] end def deps [#snip {:std_json_io, \u0026#34;~\u0026gt; 0.1.0\u0026#34;}, ] end Run a quick mix deps.get and we\u0026rsquo;re ready to get this on the road.\nIn your lib, lets create a supervisor for all those react-stdio processes.\nFor my app this is in lib/react_phx_stdio/react_io.ex:\ndefmodule ReactPhxStdio.ReactIo do use StdJsonIo, otp_app: :react_phx_stdio, script: \u0026#34;react-stdio\u0026#34; end Make sure you npm install react-stdio before you go any further.\nThen we need to add that to our supervision tree. In lib/react_phx_studio.ex (your application file):\ndef start(_type, _args) do import Supervisor.Spec, warn: false children = [ # snip supervisor(ReactPhxStdio.ReactIo, []) ] opts = [strategy: :one_for_one, name: ReactPhxStdio.Supervisor] Supervisor.start_link(children, opts) end When we restart the server now it\u0026rsquo;ll fire up a bunch of fresh react-stdio servers ready to do your bidding.\nLets get something on the page # Ok so almost all the pieces are in place to get some stuff on the page. We\u0026rsquo;ve got our server component bundle, our supervisor running a bunch of react-stdio processes and a simple component. Next we\u0026rsquo;ll add a route to spit this out to the browser.\nWe\u0026rsquo;ll create a single page controller and view to handle this for us.\ndefmodule ReactPhxStdio.SinglePageController do use ReactPhxStdio.Web, :controller def index(conn, params) do render conn, \u0026#34;index.html\u0026#34;, message: Map.get(params, \u0026#34;message\u0026#34;) end end Nothing to see here. Lets add it to the router.\nscope \u0026#34;/\u0026#34;, ReactPhxStdio do # snip forward \u0026#34;/app\u0026#34;, SinglePageController, :index end The forward call in the route is a globbing matcher. It will forward all urls starting with /app to the index action.\nThe view is where the interesting part happens:\ndefmodule ReactPhxStdio.SinglePageView do use ReactPhxStdio.Web, :view def render(\u0026#34;index.html\u0026#34;, opts) do props = %{} if opts[:message], do: props = Map.put(props, :message, opts[:message]) result = ReactPhxStdio.ReactIo.json_call!(%{ component: \u0026#34;./priv/server/js/component.js\u0026#34;, props: props, }) render \u0026#34;app.html\u0026#34;, html: result[\u0026#34;html\u0026#34;], props: props end end The interesting part is the json_call!. That sends the object to the react-stdio server. The :component key is the location of the file relative to app root, and the props, well they\u0026rsquo;re the props to send down. This is the current API of react-stdio. We\u0026rsquo;re just following that.\nOnce we have the result, we\u0026rsquo;ll push it to a template to render it and the props onto the page.\nweb/templates/single_page/app.html.eex\n\u0026lt;div id=\u0026#39;content\u0026#39;\u0026gt;\u0026lt;%= raw @html %\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt;APP_PROPS=\u0026lt;%= raw Poison.encode!(@props) %\u0026gt;\u0026lt;/script\u0026gt; If you restart your server now and visit localhost:4000/app you should see the rendered component.\nThere\u0026rsquo;s only one thing left to do now. Push down our component in our app.js so that it hijacks that pre-rendered react page.\nIn our template, we had a div with an id of content and we also splatted out the props into APP_PROPS. Lets update our app.js to take advantage of that.\nimport \u0026#34;phoenix_html\u0026#34; import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; import HelloWorld from \u0026#34;./components/main\u0026#34;; var App = React.createFactory(HelloWorld); ReactDOM.render(App(window.APP_PROPS), document.getElementById(\u0026#39;content\u0026#39;)); You shouldn\u0026rsquo;t need to restart your server, it should already have refreshed.\nUPDATE: I forgot to orignally put this in, but in development, you\u0026rsquo;re going to want to have your react-stdio servers pick up changes to your server bundle. std_json_io has you covered on this. You can let it know to watch files and when it notices that one has changed it will kill the react-stdio servers, the supervisor will start them back up - fresh and ready to go. Drop this into your config/dev.exs (Please don\u0026rsquo;t add this to any other environments. That would not be cool)\nconfig :react_phx_stdio, ReactPhxStdio.ReactIo, watch_files: [ Path.join([__DIR__, \u0026#34;../priv/server/js/component.js\u0026#34;]) # only watch files in dev ] Rendering react on the server this way feels pretty good to me. We have a couple of very loosely coupled components working together to pre-render. For my first try at OTP I\u0026rsquo;m really happy with how it turned out. I love that if there\u0026rsquo;s a problem with the JS and it dies, it will just start straight back up fresh.\nHuge props to Michael. It was amazing to see just how fast he got react-stio up and running.\n","date":"2 January 2016","externalUrl":null,"permalink":"/posts/render-react-with-phoenix/","section":"Posts","summary":"Server-side render React components with Phoenix using react-stdio and OTP for isomorphic Elixir applications.","title":"Render React with Phoenix","type":"posts"},{"content":" Written for Guardian v1 (2015). The Guardian API has changed significantly since this was written. The core concepts still apply, but check the Guardian README for current usage. So far in the Simple Guardian we\u0026rsquo;ve covered browser login, api authentication and using permissions. Today I want to cover using multiple sessions.\nThere\u0026rsquo;s many reasons you may want to have multiple sessions. You may have a \u0026lsquo;secure\u0026rsquo; part of your site that has a short lived session. You might have a team based setup where you have one session - with different permissions each - for each team, or you might have something as simple as an admin section of your site. In any of these cases Guardian can help you by allowing you to store multiple \u0026lsquo;sessions\u0026rsquo; in you browser session.\nBefore we proceed # In todays example we\u0026rsquo;re going to look at having a separate admin login. We could have chosen to use a simple permission to declare someone an admin or use a different token type and guard on that. I\u0026rsquo;ve almost always found that when you implement an admin session there are many differences in how the current user is handled for login and also admins are usually also users. To keep these concerns separate we\u0026rsquo;re going to use a different session rather than permission or token type.\nIf you want to see this type of code in action you can refer to the Phoenix Guardian example app. In that application we use Überauth so it will look a little different.\nLogin # We\u0026rsquo;re going to use code very similar to the browser login code. What we\u0026rsquo;ll end up with is two login handlers. One for normal site usage, and one for the admin section of the site. I\u0026rsquo;m going to assume that you\u0026rsquo;ve already setup the normal login section of the site and we\u0026rsquo;ll just focus on the admin part.\nThe first thing we\u0026rsquo;re going to want to do is create an admin login endpoint.\ndefmodule MyApp.Admin.SessionController do use PhoenixGuardian.Web, :controller def login(conn, params) do user = # fetch your user if user.is_admin do conn |\u0026gt; put_flash(:info, \u0026#34;Signed in as #{user.name}\u0026#34;) |\u0026gt; Guardian.Plug.sign_in(user, :token, key: :admin, perms: %{default: Guardian.Permissions.max}) |\u0026gt; redirect(to: admin_user_path(conn, :index)) else conn |\u0026gt; put_flash(:error, \u0026#34;Unauthorized\u0026#34;) |\u0026gt; redirect(to: admin_login_path(conn, :new)) end end end Here we\u0026rsquo;re using a boolean flag on the user struct is_admin to tell if someone is allowed to have access to the admin section of the site.\nThe key part to notice in this Guardian.Plug.sign_in is the key: :admin. This tells guardian to to store the token in a different location. The :admin location. The location can be anything, but it feeds into all of Guardian.Plug functions for fetching tokens, permissions, login, logout etc.\nProtecting admin endpoints # The first step to protecting your endpoints is setting up a pipeline. You\u0026rsquo;ll need a new pipeline to load the JWT from the :admin part of Guardians session. The pipeline is the same as the normal browser pipeline, but we\u0026rsquo;ll tell it to look in the different location.\npipeline :admin_browser_auth do plug Guardian.Plug.VerifySession, key: :admin plug Guardian.Plug.LoadResource, key: :admin end scope \u0026#34;/admin\u0026#34;, MyApp.Admin, as: :admin do pipe_through [:browser, :admin_browser_auth] get \u0026#34;/login\u0026#34;, SessionController, :login resources \u0026#34;/users\u0026#34;, UserController end Notice how similar it is to the browser_auth pipeline. The only difference is again the key: :admin part that tells Guardian.Plug to look in the admin part of the session.\nNow that we have an admin login and a pipeline, we can protect our admin endpoints.\ndefmodule MyApp.Admin.UserController do use PhoenixGuardian.Web, :controller alias Guardian.Plug.EnsureAuthenticated plug EnsureAuthenticated, handler: __MODULE__, key: :admin def index(conn, params) do users = Repo.all(User) render conn, \u0026#34;index.html\u0026#34;, users: users end end Again this is the same as the normal part of the site, we just use key: :admin. Once you\u0026rsquo;ve stored a token in the :admin location you use it just the same as before - only now we tell Guardian to look in the right place.\nLogout # With a separate login for admin and normal users, it\u0026rsquo;s possible that you\u0026rsquo;ll be logged in as both an admin and a normal user, giving you two sessions.\nWhen logging out we can choose to either logout entirely - both user and admin - or we can just logout the admin session.\nSay we just want to logout of the admin section of the site.\ndefmodule MyApp.Admin.SessionController do # snip plug EnsureAuthenticated, [key: :admin, handler: __MODULE__] when action in [:delete] def delete(conn, _params) do conn |\u0026gt; Guardian.Plug.sign_out(:admin) |\u0026gt; put_flash(:info, \u0026#34;Admin signed out\u0026#34;) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end end This will only logout the token found in the :admin section. If we want to logout all sessions we can just call sign_out passing only the conn struct.\ndef delete(conn, _params) do conn |\u0026gt; Guardian.Plug.sign_out |\u0026gt; put_flash(:info, \u0026#34;Logged out\u0026#34;) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end Impersonation # Now that we have normal users and admin users able to sign in at the same time, we can implement an impersonation feature. We\u0026rsquo;ll implement it on the admin side. This is straying outside of the \u0026lsquo;simple guardian\u0026rsquo; a little bit, but I\u0026rsquo;ve done impersonation so many times that I think it\u0026rsquo;s useful enough to cover.\ndefmodule MyApp.Admin.SessionController do plug EnsureAuthenticated, [key: :admin, handler: __MODULE__] when action in [:delete, :impersonate, :stop_impersonating] # snip def impersonate(conn, params) do admin = Guardian.Plug.current_resource(conn, :admin) user = Repo.get(User, params[\u0026#34;user_id\u0026#34;]) conn |\u0026gt; Guardian.Plug.sign_out(:default) |\u0026gt; Guardian.Plug.sign_in(user, :token, perms: %{default: Guardian.Permissions.max}, imp: admin.id) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end def stop_impersonating(conn, params) do conn |\u0026gt; Guardian.Plug.sign_out(:default) |\u0026gt; redirect(to: admin_user_path(conn, :index)) end end Impersonate function # In the impersonate function, first we grab the admin and the user. We then want to sign out anyone who is currently logged into the default login. This will revoke the current token in preparation for us to login the user to the default session.\nWe then login to the session as normal, but we\u0026rsquo;re going to put one extra piece of information into the token so that we know that the token is an impersonation one. We\u0026rsquo;ll put the admins id into the :imp key (note this is non-standard). The imp key will now be present in the claims on each request when the JWT is decoded.\nAt this point, I like to have a way on the front end to know that I\u0026rsquo;m impersonating and allow me to stop impersonating (logout that user from the default location). To do this I put a simple template in my layout that will be a bar at the top of the page.\n\u0026lt;%= if admin_logged_in?(@conn) \u0026amp;\u0026amp; logged_in?(@conn) do %\u0026gt; \u0026lt;div class=\u0026#39;impersonation-bar\u0026#39;\u0026gt; \u0026lt;%= link \u0026#34;Stop impersonating #{current_user(@conn).name}\u0026#34;, to: admin_session_path(@conn, :stop_impersonating), method: \u0026#34;DELETE\u0026#34;, class: \u0026#34;btn btn-xs btn-warning\u0026#34; %\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;% end %\u0026gt; Where the helper methods are imported in web.ex:\ndefmodule MyApp.ViewHelpers do def admin_logged_in?(conn) do Guardian.Plug.authenticated?(conn, :admin) end def admin_user(conn) do Guardian.Plug.current_resource(conn, :admin) end def logged_in?(conn) do Guardian.Plug.authenticated?(conn) end def current_user(conn) do Guardian.Plug.current_resource(conn) end end We\u0026rsquo;re almost done. For this to work, we need to verify the admin token in the normal site. We don\u0026rsquo;t need to load the admin resource, just verify the token. We\u0026rsquo;ll add another pipeline.\npipeline :impersonation_browser_auth do plug Guardian.Plug.VerifySession, key: :admin end scope \u0026#34;/\u0026#34;, MyApp do pipe_through [ :browser, :browser_auth, :impersonation_browser_auth ] end By verifying the token, we\u0026rsquo;ll be able to see if the admin is logged in and valid in our template for the impersonation bar.\nThat\u0026rsquo;s almost it. The stop_impersonating function is just a simple logout function where we only sign out of the :default location rather than all.\n","date":"30 December 2015","externalUrl":null,"permalink":"/posts/simple-guardian-multiple-sessions/","section":"Posts","summary":"Use Guardian to manage multiple browser sessions in your Phoenix app, such as separate user and admin logins.","title":"Simple Guardian - Multiple Sessions","type":"posts"},{"content":" Written for Guardian v1 (2015). The Guardian API has changed significantly since this was written. The core concepts still apply, but check the Guardian README for current usage. In the last couple of posts I outlined some of the simple parts of Guardian. They covered browser login and api authentication. I wanted to continue along similar lines by going over how to use Guardian to embed permissions into your tokens.\nBefore we proceed # While permissions aren\u0026rsquo;t strictly part of the JWT spec, they\u0026rsquo;re also not against it. JWT allows for arbitrary data to be encoded into the token and Guardian uses this to encode permissions into the token under a specific key.\nMost of the code I\u0026rsquo;m going to reference in this post is part of the Phoenix Guardian example app. It may change since the time of writing.\nAt the time of writing the version of Guardian is 0.9.0.\nDeclaring permissions # Permissions in Guardian are listed in the configuration. You can encode multiple permission sets, where the :default set is the default. I usually enumerate the permissions in the config/config.exs file since they\u0026rsquo;re going to be applicable for all your environments.\nconfig :guardian, Guardian, # … permissions: %{ default: [ :read_token, :revoke_token, ], } This enumerates a default list of permissions with two permissions included. Ultimately when used in a JWT these permissions will be encoded into an integer as a bit string so they\u0026rsquo;re limited in number. To conform to JSON you have 32 bits only. Don\u0026rsquo;t worry about running out though, Guardian allows you to declare multiple permission sets. Imagine that you also had a \u0026ldquo;profile\u0026rdquo; set of permissions. You\u0026rsquo;d declare those like:\nconfig :guardian, Guardian, # … permissions: %{ default: [ :read_token, :revoke_token, ], profile: [ :full, :update, :read_settings, :update_settings ] } You can add as many sets of permissions as you need to help organize your permissions and make sure you don\u0026rsquo;t run out of them.\nOne thing to bear in mind, a JWT needs to be able to fit into the header of an HTTP request. That\u0026rsquo;s partly the reason that we encode the permissions as an INT value. If you add too many the keys will start taking up a lot of space.\nNow we\u0026rsquo;ve got some permissions, let\u0026rsquo;s have a look at how to use them.\nMaking a token with permissions # When you sign in, either via api_sign_in or sign_in and even encode_and_sign you\u0026rsquo;re able to pass in a :perms param that contains a Map of\n\u0026lt;permission set\u0026gt;: \u0026lt;set of permissions\u0026gt; The permission set is the name of the permission set in the config The set of permissions can be A list of strings that appear in the declared list A list of atoms that appear in the declared list An integer of encoded permissions Guardian.Permissions.max Guardian.Permissions.max is a way to set all the bits so that all permissions in that set are granted, even if new ones are added.\nSo when you generate your token you can declare the permissions\ndef login(conn, params) do user = # get your user somehow conn |\u0026gt; put_flash(:info, \u0026#34;Signed in as #{user.name}\u0026#34;) |\u0026gt; Guardian.Plug.sign_in(user, :token, perms: %{default: Guardian.Permissions.max}) |\u0026gt; redirect(to: private_page_path(conn, :index)) end Notice here we\u0026rsquo;ve granted the max permissions for the default set. If we wanted to enumerate them we\u0026rsquo;d do it like:\ndef login(conn, params) do user = # get your user somehow conn |\u0026gt; put_flash(:info, \u0026#34;Signed in as #{user.name}\u0026#34;) |\u0026gt; Guardian.Plug.sign_in(user, :token, perms: %{default: [:read_token, :revoke_token]}) |\u0026gt; redirect(to: private_page_path(conn, :index)) end Remember though, if you do it this way any new permissions will not be present in the token.\nProtecting endpoints with Plug # A lot of times it\u0026rsquo;s possible to simply use a plug to prevent unauthorized access. To prevent access, in your controller declare a plug that requires the correct permissions.\nplug EnsurePermissions, [handler: __MODULE__, default: ~w(read_token)] when action in [:index] plug EnsurePermissions, [handler: __MODULE__, default: ~w(revoke_token)] when action in [:delete] We use the Guardian.Plug.EnsurePermissions plug to enforce permissions before it even gets to our action. I\u0026rsquo;ve aliased it in this controller. Like the EnsureAuthenticated plug from our previous examples, the EnsurePermissions plug takes a handler module. In this case our own controller. This means that we need to implement an unauthorized function on our handler module.\ndef unauthorized(conn, _params) do conn |\u0026gt; put_flash(:error, \u0026#34;Unauthorized\u0026#34;) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end One thing to note. When using the plug, all permissions listed must be present.\nFor the simple case that\u0026rsquo;s pretty much it. But if you need to get closer to the metal you can customize how you handle permissions.\nCustom endpoint protection # Sometimes the plug may not be sufficient. For example if you have an OR condition in your permission requirements Guardians plug won\u0026rsquo;t be able to help you. In those cases you can implement your own plug or just put it into your action.\nSomething like:\ndefault_permissions = Guardian.Permissions.from_claims(claims, :default) # From here we can check for permissions Guardian.Permissions.all?( default_permissions, [:read_token, :revoke_token], :default ) # OR we can check if there are any permissions Guardian.Permissions.any?( default_permissions, [:read_token, :revoke_token], :default ) The default you see here in the above refers to the name of the permission set.\nYou may need, from time to time to list out the encoded permissions, or, from a list of permissions - find the bit string value.\n# To return a list of permissions that were encoded Guardian.Permissions.to_list(default_permissions, :default) # To get the encoded value of permissions from a list of permissions Guardian.Permissions.to_value([:revoke_token], :default) ","date":"29 December 2015","externalUrl":null,"permalink":"/posts/simple-guardian-permissions/","section":"Posts","summary":"Embed permissions into JWT tokens using Guardian for fine-grained authorization in your Phoenix application.","title":"Simple Guardian - Permissions","type":"posts"},{"content":" Written for Guardian v1 (2015). The Guardian API has changed significantly since this was written. The core concepts still apply, but check the Guardian README for current usage. In the last post I went over the simple setup for Guardian to do browser authentication. This post is going to go over API authentication. It\u0026rsquo;s pretty much the same. I\u0026rsquo;m going to assume that you have Guardian configured and have your browser login setup as per the last post. Now let\u0026rsquo;s add some API magic.\nBefore we proceed # As in the last post, Guardian doesn\u0026rsquo;t do the initial challenge part of the authentication flow. It assumes that you know who your user is, and you\u0026rsquo;re wanting to do per-request authentication. Again there are 3 major parts to it.\nGenerate a token for a known resource (user) On each request, check the token and bail if it\u0026rsquo;s not valid. Logout The main difference between API and browser logins is that with an API we provide the token to the client directly rather than just putting it in the session and being magical. Rather than use the session we\u0026rsquo;re going to use the Authorization header to send the token to the application.\nLogin # Let\u0026rsquo;s get the token to the client. We can\u0026rsquo;t just put it into the session, we\u0026rsquo;re going to need to give it to them, so they can provide it back to us.\ndef login(conn, params) do case User.find_and_confirm_password(params) do {:ok, user} -\u0026gt; new_conn = Guardian.Plug.api_sign_in(conn, user) jwt = Guardian.Plug.current_token(new_conn) claims = Guardian.Plug.claims(new_conn) exp = Map.get(claims, \u0026#34;exp\u0026#34;) new_conn |\u0026gt; put_resp_header(\u0026#34;authorization\u0026#34;, \u0026#34;Bearer #{jwt}\u0026#34;) |\u0026gt; put_resp_header(\u0026#34;x-expires\u0026#34;, exp) |\u0026gt; render \u0026#34;login.json\u0026#34;, user: user, jwt: jwt, exp: exp {:error, changeset} -\u0026gt; conn |\u0026gt; put_status(401) |\u0026gt; render \u0026#34;error.json\u0026#34;, message: \u0026#34;Could not login\u0026#34; end end There looks like there\u0026rsquo;s a bit going on there, but it\u0026rsquo;s really not much. We use api_sign_in to generate a token and put it on the connection. Then we read off the JWT and fetch the expiry, so we can let the client know.\nOn Request # On each request, as before, we want to find the token, load the resource and ensure they\u0026rsquo;re logged in. There\u0026rsquo;s really only one difference from the browser example. Where to look for the token. We\u0026rsquo;re going to look for it in the header.\npipeline :api_auth do plug Guardian.Plug.VerifyHeader, realm: \u0026#34;Bearer\u0026#34; plug Guardian.Plug.LoadResource end The only difference from the browser example is that we\u0026rsquo;re going to look for the token in the authorization header. The realm part just specifies the prefix on the header value we\u0026rsquo;re going to use. In this case \u0026ldquo;Bearer\u0026rdquo;. That is, when the client supplies the header to the application it will look for a header of the form:\nAuthorization: Bearer \u0026lt;jwt\u0026gt; The realm can be almost anything, although you might get weird things happening if you use \u0026ldquo;Digest\u0026rdquo; or \u0026ldquo;Basic\u0026rdquo;.\nOn the controller side it\u0026rsquo;s the same as the browser version. We use EnsureAuthenticated to make sure that we have a valid token. Just to outline it again.\ndefmodule MyApp.Api.LoggedInController do # snip plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__ def logged_in_action(conn, params) do user = Guardian.Plug.current_resource(conn) # do your stuff end def unauthenticated(conn, _params) do conn |\u0026gt; put_status(401) |\u0026gt; render \u0026#34;error.json\u0026#34;, message: \u0026#34;Authentication required\u0026#34; end end The same as the browser example, the unauthenticated/2 function is called when there is no valid token found.\nLogout # Logout on API tokens is slightly different from the browser.\ndef logout(conn, _params) do jwt = Guardian.Plug.current_token(conn) claims = Guardian.Plug.claims(conn) Guardian.revoke!(jwt, claims) render \u0026#34;logout.json\u0026#34; end In the browser we can just flush the session, and we\u0026rsquo;re logged out. When using API tokens though the client has hold of the token. This presents a bit of a problem because when they logout there\u0026rsquo;s not much we can do to invalidate the token. If the client chooses to re-use the token it will still be acceptable. The client should \u0026lsquo;forget\u0026rsquo; the token, and then you\u0026rsquo;re logged out. If that doesn\u0026rsquo;t sound like something you like, you should use GuardianDB. GuardianDB stores each token in the DB. GuardianDB tracks each token in the DB and when we revoke! the token it is removed - rendering it useless.\nTesting # Testing API endpoints is much easier for API endpoints than browser. We just generate the JWT and include it in the request headers.\nsetup do user = create(:user) {:ok, jwt, full_claims} = Guardian.encode_and_sign(user) {:ok, %{user: user, jwt: jst, claims: full_claims}} end test \u0026#34;GET /api\u0026#34;, %{jwt: jwt} do conn = conn() |\u0026gt; put_req_header(\u0026#34;authorization\u0026#34;, \u0026#34;Bearer #{jwt}\u0026#34;) |\u0026gt; get \u0026#34;/api\u0026#34; # test things end ","date":"22 December 2015","externalUrl":null,"permalink":"/posts/simple-guardian-api-authentication/","section":"Posts","summary":"Set up API authentication with Guardian in your Phoenix application using JWT tokens.","title":"Simple Guardian - API authentication","type":"posts"},{"content":" Written for Guardian v1 (2015). The Guardian API has changed significantly since this was written. The core concepts still apply, but check the Guardian README for current usage. These posts were co-authored with Daniel Neighman, who also co-authored Warden for Ruby. I\u0026rsquo;ve been getting quite a few requests lately for how to Guardian, but only the simple parts - just let me get started. I usually get pretty excited about stuff that I write, and I tend to want to launch straight into all the fun stuff that can be done. This post is going to attempt to outline how to write the bread and butter of Guardian. How do you integrate it into your application with just the simple stuff.\nGuardian handles browser, API, channel and even socket authentication, but let\u0026rsquo;s start simple. In the browser.\nFor the setup of your application you should include all the relevant parts in you config that can be found on the Guardian README. This post is going to assume that you have configured Guardian and implemented the Guardian Serializer for you application.\nNote: this post was written at Guardian v 0.8.0\nBefore we proceed # Guardian looks after authenticating each request to your application. It doesn\u0026rsquo;t do the initial checking of passwords or fetching information from an OAuth provider. For that you can use something like Überauth or roll your own email/password using something like Comeonin. Guardian handles each request authentication. Challenging users and confirming their credentials is up to your application. Guardian assumes that you have a user representation that you\u0026rsquo;ve confirmed already.\nBrowser Authentication # When you have a session available you\u0026rsquo;ll want to get your token into the session and use that to authenticate each request. So there\u0026rsquo;s a few parts to it:\nOn login, once the app has confirmed the credentials - login the user to the current session On each request, check the credentials and fail if they\u0026rsquo;re not found. Logout I\u0026rsquo;m going to assume you have configured your application to use Guardian.\nLogin # The first part is to login the user. Lets see some code:\ndef login(conn, params) do case User.find_and_confirm_password(params) do {:ok, user} -\u0026gt; conn |\u0026gt; Guardian.Plug.sign_in(user) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) {:error, changeset} -\u0026gt; render conn, \u0026#34;login.html\u0026#34;, changeset: changeset end end The only “Guardian” part is the Guardian.Plug.sign_in line. This line generates the JWT, stores it in the session (and on the assigns) and proceeds. At this point, you\u0026rsquo;re “logged in”.\nOn Request # Each request we want to check that JWT to make sure it\u0026rsquo;s valid and identify who is the user. There are 3 main parts:\nFind the token - in our case from the session Load the resource associated with the token Note that these two steps will just find the info, they won\u0026rsquo;t prevent access. To prevent access to non-logged in users you\u0026rsquo;ll need one more step.\nEnsure authenticated - Checks that a valid token was found and bails if not. To get this part started, we\u0026rsquo;ll want a pipeline in our router. This pipeline will assume there\u0026rsquo;s a session.\npipeline :browser_auth do plug Guardian.Plug.VerifySession plug Guardian.Plug.LoadResource end Guardian.Plug.VerifySession - Finds the token in the session Guardian.Plug.LoadResource - Uses your serializer to load the resource from the found JWT (if it finds one). Wire this pipeline into your scope:\nscope \u0026#34;/\u0026#34;, MyApp do pipe_through [:browser, :browser_auth] get \u0026#34;/logged_in_page\u0026#34;, LoggedInController, :logged_in_page end At this point, Guardian will check each request and load the associated resource (user), but it will not ensure that they\u0026rsquo;re logged in.\ndefmodule LoggedInController do # snip def logged_in_page(conn, params) do user = Guardian.Plug.current_resource(conn) # user may or may not be here. end end In order to bail if a user is not logged in we have to EnsureAuthenticated.\ndefmodule LoggedInController do # snip plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__ def logged_in_page(conn, params) do user = Guardian.Plug.current_resource(conn) render \u0026#34;logged_in_page.html\u0026#34;, user: user end # handle the case where no authenticated user # was found def unauthenticated(conn, params) do conn |\u0026gt; put_status(401) |\u0026gt; put_flash(\u0026#34;Authentication required\u0026#34;) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end end The EnsureAuthenticated plug will call its handlers unauthenticated function if no valid token was found.\nThe handler is just a module that implements unauthenticated/2. This could be the current module (as in this case) or a separate module. Separate modules make sense when you have a generic handling pattern.\nLogout # To logout all sessions:\ndefmodule SessionController do # snip def logout(conn, _params) do conn |\u0026gt; Guardian.Plug.sign_out |\u0026gt; put_flash(:info, \u0026#34;Logged out\u0026#34;) |\u0026gt; redirect(to: \u0026#34;/\u0026#34;) end end This is the main parts of authenticating with Guardian for browser sessions. The outstanding part is how to test this.\nTesting # Turns out that it\u0026rsquo;s pretty tricky. We need to get the JWT into the session before we make our request. Phoenix has us covered for this. I make a helper and put it into my ConnCase.\n# We need a way to get into the connection to login a user # We need to use the bypass_through to fire the plugs in the router # and get the session fetched. def guardian_login(user, token \\\\ :token, opts \\\\ []) do conn() |\u0026gt; bypass_through(MyApp.Router, [:browser]) |\u0026gt; get(\u0026#34;/\u0026#34;) |\u0026gt; Guardian.Plug.sign_in(user, token, opts) |\u0026gt; send_resp(200, \u0026#34;Flush the session yo\u0026#34;) |\u0026gt; recycle() end This will prepare the connection and flush the token into the session. To use it we\u0026rsquo;d write a test like:\ntest \u0026#34;GET /logged_in_page\u0026#34;, %{ user: user } do conn = guardian_login(user) |\u0026gt; get(\u0026#34;/tokens\u0026#34;) assert html_response(conn, 200) end ","date":"22 December 2015","externalUrl":null,"permalink":"/posts/simple-guardian/","section":"Posts","summary":"A straightforward guide to setting up browser authentication in your Phoenix application using Guardian.","title":"Simple Guardian - Browser login","type":"posts"},{"content":" Writing Ideas # Backlog of future blog posts. Grouped by theme. When ready to write, promote to a real draft in content/posts/.\nLeadership \u0026amp; Team Building # Great vs Terrible Babysitter → Drafted # Promoted to content/posts/the-best-engineers-read-the-room.md. Parable about treating every project like you\u0026rsquo;re watching someone else\u0026rsquo;s kid. Cross-links to the handbook\u0026rsquo;s Mechanic Parable.\nHiring for Long Term # Hire someone you genuinely like who will tell you when you\u0026rsquo;re wrong. Skills can be taught. The willingness to push back on a leader? That\u0026rsquo;s character, and you can\u0026rsquo;t train it. Teams full of agreeable people ship agreeable, mediocre products.\nHigh-Churn vs Low-Churn Team Building # The strategy for building a team that turns over every 18 months is fundamentally different from building one that stays for years. Most leaders optimize for one and get the other. How you onboard, how you document, how you distribute knowledge: all of it changes based on which reality you\u0026rsquo;re actually in.\nSetting Expectations (Heads Down) # The idea behind \u0026ldquo;heads down\u0026rdquo; time and what happens when expectations aren\u0026rsquo;t set explicitly. When a team doesn\u0026rsquo;t know what\u0026rsquo;s expected of them, they fill the void with anxiety and overwork. Setting expectations is the highest-leverage, lowest-effort leadership move that almost nobody does well.\nWhat is Your Brand? # Every engineer is known for something whether they chose it or not. Better to be deliberate about it. What\u0026rsquo;s the thing people say about you when you\u0026rsquo;re not in the room? If you don\u0026rsquo;t know, someone else is deciding for you.\nCommunication \u0026amp; Writing # Written-First Culture vs Standup # The case for replacing (or radically restructuring) standups with written async updates. Standups optimize for synchronous presence. Written-first optimizes for clarity and accountability. Most teams run standups because they always have, not because they\u0026rsquo;ve evaluated the tradeoff. Connects to: Handbook rituals page\nWriting: A Walk Around Campus # The writing technique of walking someone through a space before asking them to do anything in it. Orient the reader first. Give them the lay of the land. Then zoom in. Applies to docs, tickets, architecture proposals, onboarding materials.\nWriting: Start With the Punchline # Lead with the conclusion. State what you believe and why, then support it. Business writing isn\u0026rsquo;t fiction. Don\u0026rsquo;t build suspense. The reader\u0026rsquo;s time is finite and their attention is fragile. If they only read the first sentence, did they get the point?\nGoing Remote: Being Visible # Sequel to the existing Going Remote post. In an office, presence is automatic. Remote, you\u0026rsquo;re invisible by default. The skills that make you visible remotely (writing, proactive communication, showing your work) are different from the skills that made you visible in person. Most people never make the switch deliberately. Connects to: existing post, handbook communication patterns\nEngineering Craft # Thinking in Ratios # Reframe engineering decisions as ratios, not absolutes. \u0026ldquo;This saves 10 minutes\u0026rdquo; means nothing. \u0026ldquo;This saves 10 minutes per engineer per day across 40 engineers\u0026rdquo; is a headcount. The ability to think in ratios (time × people × frequency) is what separates senior engineers from staff+ engineers. It\u0026rsquo;s also how you get budget for anything.\nPicking Your Stack # How to choose a technology stack when starting something new. The answer is almost never \u0026ldquo;the best technology.\u0026rdquo; It\u0026rsquo;s the one your team can hire for, maintain, and debug at 2am. Stack decisions are hiring decisions, maintenance decisions, and on-call decisions disguised as technical ones.\nEstimation is About Complexity # Estimation measures confidence and risk, not time. The entire industry argues about story points vs hours vs t-shirt sizes while missing the point: you\u0026rsquo;re estimating what you don\u0026rsquo;t know, not how long it takes. Reframe estimation as a complexity and uncertainty conversation and the whole practice gets more honest. Connects to: Handbook scoping page, \u0026ldquo;Knowing When to Stop\u0026rdquo; post\u0026rsquo;s estimation section\nClarity: Write Your Code Conversationally # Code should read like an explanation to a coworker. If you have to re-parse a method to understand what it does, the naming or structure failed. This isn\u0026rsquo;t about comments. It\u0026rsquo;s about choosing names, structures, and abstractions that make the code\u0026rsquo;s intent obvious to someone reading it for the first time.\nREADME-Driven Interviewing # Use your project\u0026rsquo;s README (or a representative one) as the basis for technical interviews. It reveals how candidates think about onboarding, documentation, and developer experience. Do they read before they code? Do they ask clarifying questions? Do they notice what\u0026rsquo;s missing? These are better signals than whiteboard algorithms.\nNotes # \u0026ldquo;The Mechanic\u0026rdquo; idea is already the handbook parable. No separate post needed. \u0026ldquo;Estimation is about complexity\u0026rdquo; has significant overlap with existing content. Consider whether it\u0026rsquo;s a standalone post or an expansion of the scoping handbook page. \u0026ldquo;Thinking in Ratios\u0026rdquo; and the \u0026ldquo;DX is a Leadership Problem\u0026rdquo; draft share DNA (the 35-minute deploy math). Make sure they complement rather than repeat. ","externalUrl":null,"permalink":"/ideas/","section":"eval ( code )","summary":"","title":"","type":"page"},{"content":" Hey, I\u0026rsquo;m Justin. # I build engineering teams that ship reliably without burning out.\nI\u0026rsquo;ve spent more than a decade as a founding engineer at multiple companies, building the teams and culture from zero and taking them through acquisition. That means clear processes, honest estimation, rituals that earn their keep, and a definition of \u0026ldquo;done\u0026rdquo; that actually means done. That\u0026rsquo;s what the Dev Handbook is about.\nProjects # Warden Authentication - Maintainer of the open-source Ruby authentication library. Warden is the standard in Ruby on Rails authentication with more than 125 million downloads. It powers authentication for millions of users every day.\nHeads Down - A SaaS platform I built to manage workplace interruptions from Slack. It lets users focus on the task at hand and communicate response times with coworkers when they\u0026rsquo;re busy. Native clients built with Swift (iOS) and Flutter/Kotlin (Android), with an Elixir + LiveView backend.\nExperience # Senior Staff Engineer at Shopify\nRotated across 8 teams in Growth (30 to 50 engineers each), brought in to raise the bar on technical direction, architecture, and engineering culture. Balance long-term platform investments against short-term product needs to capture market opportunities.\nLed Shopify\u0026rsquo;s first pricing change in company history, coordinating engineering, product, and business teams to ship without disruption and drive millions in additional revenue. Architected an entitlements system that now powers authorization for all Shopify customers and their contracts, eliminating thousands of developer hours of manual authorization logic. Redesigned the billing system to operate across global markets. Transformed Customer Support systems with an AI-first architecture. Elevated engineering quality and delivery pace in every team through hands-on mentorship and process change that stuck after moving on. Principal Software Engineer at Red Canary\nBuilt tooling for Fortune 500 companies to detect and combat real-time threats. The platform reliably processed over a petabyte of data per day.\nFounded two internal teams: Application Security and Developer Tools. Scaled engineering architecture and processes from 180 to 400 people in a single year. Reduced deploy times from 35 minutes to 3 minutes. Launched continuing-education programs to improve engineering quality and retention. Established coding standards and pull request review processes across the organization. Principal Software Engineer at Enzyme (YCombinator)\nBuilt a regulatory and compliance system for the pharmaceutical and medical device industries to streamline FDA approval.\nBuilt a new backend in Elixir using CQRS and Event Sourcing to meet compliance requirements and eliminate tracing issues with the existing system. Coordinated with a globally distributed engineering team to prioritize work. Replaced a broken Agile system with an engineering process that aligned with the founder\u0026rsquo;s delivery goals. Senior Software Architect at Keyp GmbH\nDesigned a system that lowered the cost of identity verification in the EU by 80% while eliminating the security risk of storing identity-related information on company systems.\nRelocated to Munich, Germany to work directly with the founding team. Advised the founders on startup growth, scaling, fundraising, and North American market expansion. Guided the company through staffing and employee-structure hurdles. Shipped multiple application MVPs using Ruby, Elixir, and Elm when the company was working through traction and funding challenges. Data Team Lead at Homebot\nFifth employee and first engineering hire. Homebot processed 30 years of public records to build custom estimation models, allowing homeowners to maximize the wealth in their homes without giving away personal information to private companies.\nBuilt the engineering team and culture from scratch. Retained 100% of hires and recruited 6 engineers from my personal network. Architected a zero-downtime migration from Node.js to Rails for better stability and performance. Built ETL pipelines using Go and Spark for analysis of real estate records. Rolled out code analysis and syntax checking tools across the team. Promoted to Data Team Lead after 8 months to own all data ingestion and analysis. VP of Engineering at AgilData\nTook over a struggling 8-year-old startup pivoting to compete in the Big Data sector against Apache Spark and Hadoop using an in-house Java-based SQL data store.\nOverhauled engineering practices to align with the new CEO\u0026rsquo;s goals. Built a customer-facing system for tracking feature requests on the product roadmap. Led all people management inside Engineering. Stood up HR and payroll systems to enable a fully remote company. Technical Director at NCC Group\nFirst engineer hired at Artemis (acquired by NCC Group) to launch the .trust security-focused gTLD. Designed the engineering organization and architecture to meet ICANN compliance requirements for the .trust domain. Every domain was scanned in real-time for security vulnerabilities and locked down within seconds to prevent attacks.\nHired by Alex Stamos to build the engineering team at Artemis, spanning software architecture, project management, and recruiting. Promoted to Technical Director after one year. Managed 8 direct reports and provided technical direction for 16. Built a hybrid engineering team across 3 offices with full-time remote employees. Built domain registrar, registry, and certificate issuer systems using Ruby and Go. Designed a system architecture resilient to nation-state attacks, mitigating the potential for zero-day exploits. Founder of Mongo Machine\nBuilt a SaaS business specializing in customized high-availability deployments of MongoDB. Bootstrapped to profitability as a solo founder and acquired by Compose in 2011. Compose was later acquired by IBM.\nEducation # B.S. Computer Science, University of Iowa\nAll thoughts and opinions on this site are my own. They do not represent Shopify or any other employer.\nAbout this site # This blog covers engineering topics from career management to programming techniques. Most of the content is focused on Elixir, Ruby, and the tools and practices that make remote teams work.\nThe site is built with Hugo and the Blowfish theme.\n","externalUrl":null,"permalink":"/about/","section":"eval ( code )","summary":"Serial founding engineer who has built teams from zero to exit at multiple companies. Senior Staff Engineer at Shopify. More than a decade of remote engineering leadership.","title":"About Justin Smestad","type":"page"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"In this section:\n📖 The Mechanic Parable: A Story About Expectations 🌐 Going Remote: Replacing What the Office Gave You for Free 🔭 From Problem to Feature: Scoping Work ☀️ Project Lead\u0026rsquo;s Daily Routine 📜 Your Project Board is a Mirror 🥁 Rituals: Not Every Team Needs Every Meeting 🧾 Templates: Don\u0026rsquo;t Start From a Blank Page What # This is a practical guide for anyone leading, or growing into leading, a remote dev team. It covers the stuff that trips teams up the most: writing clear tickets, estimating without lying to yourself, running rituals that people actually find useful, and knowing when \u0026ldquo;done\u0026rdquo; really means done.\nYou don\u0026rsquo;t need to be a tech lead to get value here. If you\u0026rsquo;ve ever stared at a ticket wondering what it\u0026rsquo;s actually asking for, or sat through a standup that felt pointless, this handbook is for you.\nWhy # Most teams don\u0026rsquo;t have a broken process. They have half a process. Standups that don\u0026rsquo;t connect to the board. Tickets that don\u0026rsquo;t describe what \u0026ldquo;done\u0026rdquo; looks like. Estimates that nobody trusts, including the person who gave them.\nThe problem is that each piece was adopted in isolation. Someone read a blog post about standups, someone else copy-pasted a ticket template from a previous job, and now you\u0026rsquo;ve got a Frankenstein workflow where nothing feeds into anything else.\nThis handbook explains how all the pieces fit together. Scoping feeds estimation. Estimation sets expectations. Expectations drive standups. Standups surface blockers. The board reflects reality. When one part is out of sync, the whole system drifts. When they connect, the team spends less time talking about the work and more time doing it.\n","externalUrl":null,"permalink":"/handbook/","section":"Dev Handbook","summary":"A practical guide for anyone leading, or growing into leading, a remote dev team.","title":"Dev Handbook","type":"handbook"}]