[{"data":1,"prerenderedAt":8248},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-12":4,"blog-paginated-cats":7603},640,[5,243,558,886,1118,1996,3428,4173,4879,5706,5986,6297,6564,6900,7306],{"id":6,"title":7,"author":8,"body":11,"category":223,"date":224,"description":225,"extension":226,"featured":227,"image":228,"keywords":229,"meta":232,"navigation":233,"path":234,"readTime":235,"seo":236,"stem":237,"tags":238,"__hash__":242},"blog/blog/saas-pricing-models.md","SaaS Pricing Models: Per Seat, Usage-Based, and Everything In Between",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":212},"minimark",[14,19,23,26,29,33,40,43,49,52,58,61,67,69,73,76,79,82,85,88,90,94,97,103,109,115,117,120,123,126,129,132,134,138,141,147,153,159,165,167,178,180,184],[15,16,18],"h2",{"id":17},"pricing-is-a-product-decision","Pricing Is a Product Decision",[20,21,22],"p",{},"Most SaaS founders treat pricing as a finance or marketing problem. It's neither — it's a product problem. The pricing model you choose determines who can buy, how they buy, how they experience value, and how you scale revenue with your customers. Get it wrong and you'll either leave money on the table or create friction that prevents adoption.",[20,24,25],{},"The good news is there are only a handful of primary models, and the right one for your product isn't usually that hard to identify if you start with the right questions.",[27,28],"hr",{},[15,30,32],{"id":31},"the-core-models","The Core Models",[20,34,35,39],{},[36,37,38],"strong",{},"Per seat (per user) pricing."," The customer pays a fixed amount per user per month. Salesforce, Slack, HubSpot, Notion — this is the dominant model for collaboration and productivity software. It's intuitive, easy to explain, and scales predictably with the customer's team size.",[20,41,42],{},"The challenge with per seat: it creates an incentive to reduce users. A $50/seat/month product with 20 users creates $1,000/month in motivation to share credentials, use service accounts, or limit who gets access. This is particularly acute in budget-sensitive companies, and it caps your revenue at the size of the buyer's team rather than the value they're getting.",[20,44,45,48],{},[36,46,47],{},"Usage-based pricing."," The customer pays for what they use — API calls, data processed, emails sent, AI tokens consumed. AWS, Twilio, Stripe, Snowflake — this model is dominant in infrastructure and API products. Revenue grows automatically as customers get more value. You don't need to convince anyone to buy more seats; as they use more, they pay more.",[20,50,51],{},"The challenge: unpredictable billing makes buyers nervous, especially enterprises that need to budget. Usage-based products often add complexity to their pricing to add a floor (minimum monthly commitment) or ceiling (enterprise flat rate), which creates a hybrid model whether you intended one or not.",[20,53,54,57],{},[36,55,56],{},"Flat rate (tiered by feature)."," Three plans — Starter, Professional, Enterprise — with different feature sets at fixed monthly prices. This is what most SaaS products start with because it's easy to build and easy to explain. Pay $49/month for the basic plan or $199/month for everything.",[20,59,60],{},"The challenge: you're selling the same thing to the customer with 2 users and the customer with 200. The value delivered is wildly different; the price is the same. Tiered flat rate pricing leaves money on the table at the top and creates artificial constraints at the bottom.",[20,62,63,66],{},[36,64,65],{},"Outcome-based pricing."," You charge based on the business outcome the product produces — a percentage of revenue generated, a fee per qualified lead, a share of cost savings. This is the highest-value model when you can implement it because it directly aligns your price with the customer's value. It's also the hardest to measure and audit, and it requires deep trust between you and your customer.",[27,68],{},[15,70,72],{"id":71},"how-to-choose","How to Choose",[20,74,75],{},"Start with two questions: what is the primary unit of value your product creates, and who bears the cost sensitivity?",[20,77,78],{},"If value scales with users (a collaboration tool, a project management system), per seat is natural alignment. More users means more value delivered, so more users means more revenue.",[20,80,81],{},"If value scales with usage (API calls, transactions processed, data volume), usage-based is the honest model. It removes the friction of seat-based constraints and creates automatic expansion revenue.",[20,83,84],{},"If your product creates lumpy value — either you use it or you don't, and the value doesn't scale linearly — flat rate or tiered pricing may be the cleaner choice. A tool that runs your CI/CD pipeline doesn't get more valuable because you have more engineers using it; it's either running your builds or it isn't.",[20,86,87],{},"If your buyer is an enterprise with a procurement process, predictability matters more than optimization. Enterprise buyers want to know what they'll pay in Q3 so they can get it approved in Q2 planning. Usage-based pricing with no ceiling is hard to approve. Flat rate with an enterprise tier is easier.",[27,89],{},[15,91,93],{"id":92},"the-hybrid-models-that-work","The Hybrid Models That Work",[20,95,96],{},"Most mature SaaS products don't run pure versions of any single model. They run hybrids that combine the predictability buyers want with the expansion economics founders want.",[20,98,99,102],{},[36,100,101],{},"Base + usage."," A fixed monthly platform fee (predictable for the customer, guaranteed revenue for you) plus usage charges above a threshold. Twilio's core SMS/voice APIs work this way. Customers know they'll pay at least X and plan accordingly; heavy users pay proportionally more.",[20,104,105,108],{},[36,106,107],{},"Per seat with usage limits."," Each seat comes with a usage allocation (X API calls per user, Y data processed per month). This lets you serve light users cheaply and capture more value from heavy users who upgrade or pay overage.",[20,110,111,114],{},[36,112,113],{},"Flat rate with seat expansion pricing."," Plans are differentiated by features, but enterprise tiers add a per-seat component. You get simple pricing at the low end and scalable revenue at the high end.",[27,116],{},[20,118,119],{},"The Technical Implications of Your Pricing Model",[20,121,122],{},"Your pricing model has engineering requirements that are easy to underestimate.",[20,124,125],{},"Usage-based pricing requires a usage metering system — tracking what each customer uses, in real time, with enough granularity to bill accurately. This is more complex than it sounds. You need to handle high volumes of events without losing data, aggregate them correctly by billing period, and report them accurately to both customers and your billing system (usually Stripe's metered billing).",[20,127,128],{},"Per seat pricing requires a seat management system — tracking active seats, handling seat additions and removals, and calculating prorated charges when plans change mid-billing cycle. Stripe's subscription API handles this well if you model it correctly from the start.",[20,130,131],{},"Tiered pricing by feature requires feature flagging — a system that controls which features each customer has access to based on their plan. Build a proper feature flag system early rather than scattering plan checks throughout the codebase.",[27,133],{},[15,135,137],{"id":136},"pricing-changes-how-to-handle-them-without-losing-customers","Pricing Changes: How to Handle Them Without Losing Customers",[20,139,140],{},"You will change your pricing. Every SaaS product does, usually multiple times. The way to do it without destroying trust:",[20,142,143,146],{},[36,144,145],{},"Grandfather existing customers."," Customers on your old pricing should stay on it. Moving existing customers to a new, higher price is the fastest way to generate churn and public complaints. Make grandfathering the default policy.",[20,148,149,152],{},[36,150,151],{},"Give ample notice."," Any pricing change that affects existing customers needs at minimum 60-90 days notice, ideally more. This isn't just courtesy — it's the kind of change that affects people's budgets.",[20,154,155,158],{},[36,156,157],{},"Offer an annual upgrade path."," Before announcing a price increase, offer existing customers the opportunity to lock in current pricing for one or two years by switching to annual billing. This gives you cash up front and keeps customers happy.",[20,160,161,164],{},[36,162,163],{},"Frame changes around value additions."," \"We're increasing prices to invest in X, Y, and Z improvements\" lands better than \"due to increased costs.\" Even if both are true.",[27,166],{},[20,168,169,170,177],{},"Your pricing model is one of the highest-leverage decisions in your SaaS business — it affects every acquisition conversation, every expansion opportunity, and your annual revenue ceiling. If you're about to launch or re-price a SaaS product and want a second perspective, book a call at ",[171,172,176],"a",{"href":173,"rel":174},"https://calendly.com/jamesrossjr",[175],"nofollow","calendly.com/jamesrossjr",".",[27,179],{},[15,181,183],{"id":182},"keep-reading","Keep Reading",[185,186,187,194,200,206],"ul",{},[188,189,190],"li",{},[171,191,193],{"href":192},"/blog/pricing-software-projects","Pricing Custom Software Projects: The Framework That Works",[188,195,196],{},[171,197,199],{"href":198},"/blog/saas-metrics-to-track","SaaS Metrics That Actually Matter (And How to Track Them in Code)",[188,201,202],{},[171,203,205],{"href":204},"/blog/freelance-developer-vs-agency","Freelance Developer vs Software Agency: How to Choose the Right Partner",[188,207,208],{},[171,209,211],{"href":210},"/blog/hiring-software-development-company","Hiring a Software Development Company: What to Look For, What to Avoid",{"title":213,"searchDepth":214,"depth":214,"links":215},"",3,[216,218,219,220,221,222],{"id":17,"depth":217,"text":18},2,{"id":31,"depth":217,"text":32},{"id":71,"depth":217,"text":72},{"id":92,"depth":217,"text":93},{"id":136,"depth":217,"text":137},{"id":182,"depth":217,"text":183},"Business","2026-03-03","Your pricing model is a product decision, not just a finance decision. Here's how to choose between per seat, usage-based, flat rate, and hybrid pricing — and what each signals.","md",false,null,[230,231],"SaaS pricing models","SaaS business model",{},true,"/blog/saas-pricing-models",7,{"title":7,"description":225},"blog/saas-pricing-models",[239,240,241],"SaaS","Pricing","Business Strategy","DcGQQlx3etbTFhT3SldfxEIVd79QcIDLUVNkx6xXP-w",{"id":244,"title":245,"author":246,"body":247,"category":545,"date":224,"description":546,"extension":226,"featured":227,"image":228,"keywords":547,"meta":550,"navigation":233,"path":551,"readTime":235,"seo":552,"stem":553,"tags":554,"__hash__":557},"blog/blog/saas-security-guide.md","SaaS Security: The Non-Negotiables Before You Launch",{"name":9,"bio":10},{"type":12,"value":248,"toc":535},[249,253,256,259,262,264,268,274,280,291,297,303,305,309,315,321,331,337,339,343,346,352,362,368,370,374,377,386,389,391,395,398,405,408,410,414,417,496,498,505,507,509],[15,250,252],{"id":251},"security-debt-is-different-from-technical-debt","Security Debt Is Different From Technical Debt",[20,254,255],{},"Technical debt is the accumulated cost of shortcuts taken during development. You can carry a lot of technical debt and still ship a product that works. Security debt is different — it's a liability that sits dormant until someone decides to exploit it, and then it's not a gradual cost. It's an incident.",[20,257,258],{},"A data breach in a SaaS product doesn't just cost the remediation expense. It costs customer trust, regulatory exposure (GDPR fines can reach 4% of annual global revenue), legal liability, and often the company itself. In markets where customers are choosing between you and a competitor, a publicized breach can be the decision that sends them somewhere else.",[20,260,261],{},"The security baseline in this guide is not a complete security program. It's the minimum that every SaaS product needs before it handles real customer data.",[27,263],{},[15,265,267],{"id":266},"authentication-security","Authentication Security",[20,269,270,273],{},[36,271,272],{},"Password hashing."," Passwords must be stored as hashes using bcrypt, Argon2, or scrypt. Never MD5, SHA-1, or plain SHA-256 — these are too fast to brute-force. Argon2id is my current recommendation: it's memory-hard (resistant to GPU-based attacks) and has an excellent TypeScript library. The cost factor should be set so hashing takes 100-300ms on your server hardware.",[20,275,276,279],{},[36,277,278],{},"Password policies."," Enforce a minimum of 12 characters. Allow passphrases. Don't impose arbitrary complexity requirements (capital + number + symbol) — NIST 800-63B retired this guidance in 2017. Do check passwords against known breach lists using an API like HaveIBeenPwned or the k-anonymity endpoint.",[20,281,282,285,286,290],{},[36,283,284],{},"Multi-factor authentication."," Implement TOTP (authenticator app) as a minimum. This should be mandatory for admin roles and optional (strongly encouraged) for regular users. Libraries like ",[287,288,289],"code",{},"otpauth"," handle the TOTP implementation. Do not implement SMS-only 2FA — SS7 vulnerabilities make SMS TOTP a weaker signal than TOTP apps.",[20,292,293,296],{},[36,294,295],{},"Session management."," Sessions should expire after a period of inactivity (15-30 minutes for sensitive applications, longer for low-risk tools). Provide explicit session listing and revocation (\"these are all devices logged in, click to revoke\"). On password reset, revoke all existing sessions.",[20,298,299,302],{},[36,300,301],{},"Rate limiting on auth endpoints."," Login, password reset, and OTP validation endpoints must be rate-limited. A login endpoint that allows unlimited guesses is an invitation to credential stuffing. Use exponential backoff (5 failed attempts triggers a 15-minute lockout, with increasing delays), and consider adding CAPTCHA after 3 failures.",[27,304],{},[15,306,308],{"id":307},"data-security","Data Security",[20,310,311,314],{},[36,312,313],{},"Encryption at rest."," Use your cloud provider's encrypted volumes (AWS EBS, GCP Persistent Disk with CMEK). For databases, enable encryption at rest in the database configuration. For particularly sensitive fields (SSNs, payment data, PII beyond contact information), consider column-level encryption using a symmetric key stored in a secrets manager (AWS Secrets Manager, HashiCorp Vault), not in your application config.",[20,316,317,320],{},[36,318,319],{},"Encryption in transit."," TLS 1.2 minimum, TLS 1.3 preferred, everywhere. No HTTP endpoints that handle data. HSTS header with a long max-age. No mixed content.",[20,322,323,326,327,330],{},[36,324,325],{},"Secrets management."," API keys, database passwords, JWT secrets — none of these should appear in your application code, source control, or ",[287,328,329],{},".env"," files that get committed. Use a secrets manager or environment variable injection at deployment time. Rotate secrets on schedule (every 90 days for long-lived keys, more frequently for high-risk credentials).",[20,332,333,336],{},[36,334,335],{},"SQL injection prevention."," Use parameterized queries. If you're using an ORM (Prisma, Sequelize, TypeORM), the query builder generates parameterized queries by default for standard operations. Be extremely careful with any code that builds SQL strings dynamically — validate and sanitize every input, and prefer the ORM's query API over raw string interpolation.",[27,338],{},[15,340,342],{"id":341},"authorization-the-vulnerability-most-developers-underestimate","Authorization: The Vulnerability Most Developers Underestimate",[20,344,345],{},"The OWASP Top 10 consistently puts broken access control at the top of the list, and with good reason. Authentication confirms who you are. Authorization determines what you can do. Authorization bugs are the most common category of serious vulnerability in SaaS applications.",[20,347,348,351],{},[36,349,350],{},"Enforce authorization at the server, not the client."," Hiding a button in the UI is not access control. Every API endpoint must verify that the authenticated user has permission to perform the requested action on the requested resource.",[20,353,354,357,358,361],{},[36,355,356],{},"Tenant isolation is mandatory."," Every database query that returns resources must filter by the authenticated user's organization. A query for ",[287,359,360],{},"/api/projects"," that returns all projects in the database is a multi-tenancy breach waiting to happen. Use a middleware layer or query helper that automatically applies the tenant scope.",[20,363,364,367],{},[36,365,366],{},"Test authorization explicitly."," Write tests that verify: user A cannot access user B's resources, a member cannot perform admin actions, an unauthenticated request is rejected. These are not edge cases — they're the primary test cases for your authorization layer.",[27,369],{},[15,371,373],{"id":372},"http-security-headers","HTTP Security Headers",[20,375,376],{},"These headers should be present on every response from your application:",[378,379,384],"pre",{"className":380,"code":382,"language":383},[381],"language-text","Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'\nX-Frame-Options: DENY\nX-Content-Type-Options: nosniff\nReferrer-Policy: strict-origin-when-cross-origin\nPermissions-Policy: camera=(), microphone=(), geolocation=()\nStrict-Transport-Security: max-age=31536000; includeSubDomains; preload\n","text",[287,385,382],{"__ignoreMap":213},[20,387,388],{},"The Content-Security-Policy header is the most impactful and the most complex. It restricts what scripts can run on your pages, preventing XSS attacks even if an attacker manages to inject script content. A properly configured CSP is one of the most effective defenses against a large class of browser-based attacks.",[27,390],{},[15,392,394],{"id":393},"dependency-vulnerabilities","Dependency Vulnerabilities",[20,396,397],{},"Your application's dependencies are part of your attack surface. Libraries you installed and never updated may have known vulnerabilities that are publicly documented and actively exploited.",[20,399,400,401,404],{},"Run ",[287,402,403],{},"npm audit"," (or the equivalent for your package manager) in CI. Fail the build if any high or critical vulnerabilities are present. Use Dependabot or Renovate to automate pull requests for dependency updates. Review the changelog when upgrading major versions.",[20,406,407],{},"This is a maintenance discipline, not a one-time fix. Vulnerabilities are discovered continuously.",[27,409],{},[15,411,413],{"id":412},"the-pre-launch-security-checklist","The Pre-Launch Security Checklist",[20,415,416],{},"Before handling real customer data:",[185,418,421,430,436,442,448,454,460,466,472,478,484,490],{"className":419},[420],"contains-task-list",[188,422,425,429],{"className":423},[424],"task-list-item",[426,427],"input",{"disabled":233,"type":428},"checkbox"," All passwords hashed with bcrypt or Argon2id",[188,431,433,435],{"className":432},[424],[426,434],{"disabled":233,"type":428}," MFA implemented for admin accounts",[188,437,439,441],{"className":438},[424],[426,440],{"disabled":233,"type":428}," Auth endpoints rate-limited",[188,443,445,447],{"className":444},[424],[426,446],{"disabled":233,"type":428}," All API endpoints enforce authorization",[188,449,451,453],{"className":450},[424],[426,452],{"disabled":233,"type":428}," Tenant isolation tested: user A cannot access user B's data",[188,455,457,459],{"className":456},[424],[426,458],{"disabled":233,"type":428}," TLS everywhere, HSTS enabled",[188,461,463,465],{"className":462},[424],[426,464],{"disabled":233,"type":428}," Security headers configured (CSP, X-Frame-Options, etc.)",[188,467,469,471],{"className":468},[424],[426,470],{"disabled":233,"type":428}," No secrets in source control",[188,473,475,477],{"className":474},[424],[426,476],{"disabled":233,"type":428}," SQL injection: ORM used or parameterized queries throughout",[188,479,481,483],{"className":480},[424],[426,482],{"disabled":233,"type":428}," Dependency audit clean (no high/critical vulnerabilities)",[188,485,487,489],{"className":486},[424],[426,488],{"disabled":233,"type":428}," Error messages don't expose stack traces or internal paths in production",[188,491,493,495],{"className":492},[424],[426,494],{"disabled":233,"type":428}," Logging captures security events (login, permission denied, API key usage) without logging passwords or tokens",[27,497],{},[20,499,500,501,504],{},"Security in SaaS is not a feature you add later. It's a set of engineering practices that need to be standard from day one. If you're preparing to launch a SaaS product and want a security review of your application, book a call at ",[171,502,176],{"href":173,"rel":503},[175]," — catching these issues before launch is significantly less painful than after.",[27,506],{},[15,508,183],{"id":182},[185,510,511,517,523,529],{},[188,512,513],{},[171,514,516],{"href":515},"/blog/saas-feature-flags","Feature Flags in SaaS: Shipping Safely and Testing in Production",[188,518,519],{},[171,520,522],{"href":521},"/blog/postgresql-row-level-security","PostgreSQL Row-Level Security: Data Isolation at the Database Layer",[188,524,525],{},[171,526,528],{"href":527},"/blog/saas-development-guide","SaaS Development Guide: From Idea to Paying Customers",[188,530,531],{},[171,532,534],{"href":533},"/blog/saas-onboarding-best-practices","SaaS Onboarding: The Technical and UX Decisions That Determine Activation",{"title":213,"searchDepth":214,"depth":214,"links":536},[537,538,539,540,541,542,543,544],{"id":251,"depth":217,"text":252},{"id":266,"depth":217,"text":267},{"id":307,"depth":217,"text":308},{"id":341,"depth":217,"text":342},{"id":372,"depth":217,"text":373},{"id":393,"depth":217,"text":394},{"id":412,"depth":217,"text":413},{"id":182,"depth":217,"text":183},"Engineering","Security shortcuts in early SaaS products create liabilities that are expensive to fix and fatal if exploited. Here's the security baseline every SaaS product needs before launch.",[548,549],"SaaS security","SaaS application security",{},"/blog/saas-security-guide",{"title":245,"description":546},"blog/saas-security-guide",[555,239,556],"Security","Application Security","CWqMhJiGsgSrf4gXrshR4bltVT3ukN7wEGHE1bna_hs",{"id":559,"title":560,"author":561,"body":562,"category":545,"date":224,"description":871,"extension":226,"featured":227,"image":228,"keywords":872,"meta":875,"navigation":233,"path":876,"readTime":877,"seo":878,"stem":879,"tags":880,"__hash__":885},"blog/blog/saas-vs-on-premise.md","SaaS vs On-Premise Enterprise Software: How to Make the Right Call",{"name":9,"bio":10},{"type":12,"value":563,"toc":860},[564,568,571,574,577,580,584,587,592,606,609,612,616,619,624,644,649,669,672,675,678,681,685,688,693,696,699,713,718,721,724,728,731,734,740,746,752,758,762,765,768,782,785,789,792,798,804,810,813,817,820,823,830,832,834],[15,565,567],{"id":566},"the-default-has-shifted-but-defaults-arent-always-right","The Default Has Shifted, But Defaults Aren't Always Right",[20,569,570],{},"Five years ago, the SaaS narrative was near-universal: cloud is the future, on-premise is legacy, anyone still running their own servers is behind the times. The consulting industry sold this hard. Vendors made on-premise options intentionally inconvenient.",[20,572,573],{},"The narrative has started to reverse. Large enterprises with consistent workloads are moving back to owned infrastructure because cloud costs at scale are eye-watering. Regulatory pressure in Europe and certain regulated industries is making data sovereignty a real requirement, not just a preference. Companies that gave up on-premise options for \"simplicity\" are finding that SaaS operational costs, integration complexity, and data portability constraints are not actually simpler — just differently complex.",[20,575,576],{},"The right answer is not SaaS or on-premise as a blanket policy. It's a deployment model decision that should be made per-system, based on specific factors.",[20,578,579],{},"Here's the framework I use.",[15,581,583],{"id":582},"factor-1-data-sovereignty-and-compliance-requirements","Factor 1: Data Sovereignty and Compliance Requirements",[20,585,586],{},"This is often the deciding factor for large enterprises, healthcare organizations, financial institutions, and anyone operating in the EU under GDPR's data residency provisions.",[20,588,589],{},[36,590,591],{},"Questions to answer:",[185,593,594,597,600,603],{},[188,595,596],{},"Does your regulatory environment require you to know exactly where your data is stored?",[188,598,599],{},"Do you have contractual commitments to customers about data residency?",[188,601,602],{},"Does your industry have specific requirements about data sharing with third parties (which your SaaS vendor becomes)?",[188,604,605],{},"What is your data retention and deletion obligation, and can you verify compliance with a SaaS vendor?",[20,607,608],{},"For most small and mid-market businesses, SaaS vendors can satisfy compliance requirements. For healthcare organizations subject to HIPAA, the Business Associate Agreement (BAA) structure allows SaaS with appropriate controls. For financial institutions, specific regulations about data residency may require on-premise or private cloud deployments.",[20,610,611],{},"The key is to understand your actual requirements — not to assume compliance blocks SaaS, but to verify what your specific obligations require before assuming SaaS is acceptable.",[15,613,615],{"id":614},"factor-2-total-cost-of-ownership-over-a-5-year-horizon","Factor 2: Total Cost of Ownership Over a 5-Year Horizon",[20,617,618],{},"This calculation is often done incorrectly because the costs are on different time horizons and different balance sheet lines.",[20,620,621],{},[36,622,623],{},"SaaS cost components:",[185,625,626,629,632,635,638,641],{},[188,627,628],{},"Subscription fee (per seat, per usage, or flat rate — and what does growth cost?)",[188,630,631],{},"Implementation and onboarding (often underestimated at sales time)",[188,633,634],{},"Integration development to connect SaaS to your existing systems",[188,636,637],{},"Ongoing admin and configuration labor",[188,639,640],{},"API integration maintenance when the vendor releases breaking changes",[188,642,643],{},"Data export and migration costs if you ever need to leave",[20,645,646],{},[36,647,648],{},"On-premise cost components:",[185,650,651,654,657,660,663,666],{},[188,652,653],{},"Software license (one-time or annual maintenance)",[188,655,656],{},"Infrastructure: servers, storage, networking, data center or hosted private cloud",[188,658,659],{},"IT labor for installation, patching, upgrades, monitoring",[188,661,662],{},"Database administration",[188,664,665],{},"Backup and disaster recovery infrastructure",[188,667,668],{},"Security infrastructure (patching, vulnerability management)",[20,670,671],{},"The crossover point varies by system complexity and organization size. Generally:",[20,673,674],{},"For organizations under 100 users, SaaS is almost always lower cost over five years — the infrastructure and IT labor burden of on-premise is hard to justify at small scale.",[20,676,677],{},"For organizations over 1,000 users with stable, predictable workloads, the calculation often favors on-premise or private cloud — SaaS per-seat costs at scale can exceed on-premise TCO, sometimes dramatically.",[20,679,680],{},"In the 100-1,000 user range, the answer depends heavily on how much customization and integration you need (which erodes SaaS's simplicity advantage) and what your IT capacity looks like.",[15,682,684],{"id":683},"factor-3-customization-and-integration-requirements","Factor 3: Customization and Integration Requirements",[20,686,687],{},"SaaS vendors build for the median customer. If you're the median customer — standard use cases, standard integrations, standard workflows — SaaS is great. If you're not, the gaps start costing money.",[20,689,690],{},[36,691,692],{},"SaaS customization limits:",[20,694,695],{},"Every SaaS product has a customization ceiling. You can configure fields, workflows, and reports up to whatever the vendor built. Beyond that, you need workarounds or you live without the feature. Custom development in SaaS is either impossible or mediated through an expensive partner ecosystem.",[20,697,698],{},"When SaaS customization limits require workarounds:",[185,700,701,704,707,710],{},[188,702,703],{},"Data gets exported and manipulated in spreadsheets",[188,705,706],{},"Multiple systems emerge to handle cases the primary system can't",[188,708,709],{},"Integration complexity increases as you stitch systems together",[188,711,712],{},"Your actual total cost of ownership grows significantly above the subscription line",[20,714,715],{},[36,716,717],{},"On-premise integration flexibility:",[20,719,720],{},"With an on-premise system, your development team can build whatever integrations the business needs. You control the database. You can write custom reports against the actual tables. You can build integrations that handle edge cases a SaaS API never anticipated.",[20,722,723],{},"This flexibility has costs — development time, testing, maintenance — but for businesses with complex integration requirements, it can be significantly cheaper than the workarounds and multiple-system complexity that SaaS limitations produce.",[15,725,727],{"id":726},"factor-4-vendor-risk-and-lock-in","Factor 4: Vendor Risk and Lock-In",[20,729,730],{},"This factor gets less attention than it deserves.",[20,732,733],{},"SaaS creates a different kind of dependency than on-premise. You're dependent on the vendor's continued operation, pricing decisions, and product direction. These risks are real:",[20,735,736,739],{},[36,737,738],{},"Pricing risk."," Vendors change their pricing. A SaaS tool you budget at $50K/year can become $150K/year through a combination of seat expansion, feature tier changes, and contract renegotiation. With on-premise software, your license is your license — you don't get repriced annually.",[20,741,742,745],{},[36,743,744],{},"Product direction risk."," Vendors sunset features, pivot product direction, or get acquired. The feature that's core to your workflow today might be deprecated in two years. With on-premise, the version you're running keeps running.",[20,747,748,751],{},[36,749,750],{},"Portability risk."," Getting your data out of a SaaS system is often harder than getting it in. Data export APIs may be limited. Data formats may require transformation. Migration projects from SaaS to an alternative can be expensive. Before committing to a SaaS platform, understand exactly what your data portability rights are.",[20,753,754,757],{},[36,755,756],{},"Operational dependency."," When your SaaS vendor has an outage, your operations stop. This is true of any dependency — on-premise systems fail too — but the concentration risk of being entirely dependent on a third party's availability is a real consideration for mission-critical systems.",[15,759,761],{"id":760},"factor-5-internet-connectivity-and-performance-requirements","Factor 5: Internet Connectivity and Performance Requirements",[20,763,764],{},"This factor is underweighted in the SaaS-enthusiastic era but matters for specific use cases.",[20,766,767],{},"SaaS requires a reliable internet connection. For most office-based businesses this isn't a significant issue. For:",[185,769,770,773,776,779],{},[188,771,772],{},"Manufacturing facilities or warehouses in areas with unreliable connectivity",[188,774,775],{},"Mobile operations (field service, logistics) where cellular coverage is inconsistent",[188,777,778],{},"Applications where network latency materially affects user experience",[188,780,781],{},"Systems that process very large data volumes where bandwidth costs are real",[20,783,784],{},"On-premise or hybrid deployments with local caching provide resilience that SaaS can't match.",[15,786,788],{"id":787},"the-hybrid-model-private-cloud-and-self-hosted-saas","The Hybrid Model: Private Cloud and Self-Hosted SaaS",[20,790,791],{},"The binary SaaS vs. On-premise framing misses the middle ground that's increasingly common.",[20,793,794,797],{},[36,795,796],{},"Private cloud:"," Your infrastructure, either at a co-location facility or a dedicated cloud environment, running software you control. You get the operational benefits of managed infrastructure without the data sovereignty compromises of shared SaaS.",[20,799,800,803],{},[36,801,802],{},"Self-hosted SaaS:"," Many software products are available in both SaaS and self-hosted versions. GitLab, Bitwarden, Matomo, and many others offer you the option to run their software on your infrastructure. You manage the operations but own the data.",[20,805,806,809],{},[36,807,808],{},"Hybrid deployment:"," On-premise for the sensitive, high-volume, or regulatory data; SaaS for the collaboration and lightweight tools that don't require the same controls.",[20,811,812],{},"Most large enterprises end up with a deliberate hybrid strategy rather than a uniform policy.",[15,814,816],{"id":815},"making-the-decision-without-the-vendors-influence","Making the Decision Without the Vendor's Influence",[20,818,819],{},"One practical note: this decision is often most heavily influenced by the people trying to sell you something. SaaS vendors emphasize operational simplicity and innovation velocity. On-premise vendors emphasize control and security. Both are presenting selective truths.",[20,821,822],{},"Make the decision from your requirements, not from a vendor presentation. Write down what matters to your business — data sovereignty, cost, customization, integration — and score your options against those factors. The decision that survives that analysis is more reliable than the one that came from the most compelling demo.",[20,824,825,826,177],{},"If you're working through a deployment model decision for an enterprise system and want a second opinion from someone without a stake in which answer you pick, ",[171,827,829],{"href":173,"rel":828},[175],"schedule a conversation at calendly.com/jamesrossjr",[27,831],{},[15,833,183],{"id":182},[185,835,836,842,848,854],{},[188,837,838],{},[171,839,841],{"href":840},"/blog/build-vs-buy-enterprise-software","Build vs Buy Enterprise Software: A Framework for the Decision",[188,843,844],{},[171,845,847],{"href":846},"/blog/enterprise-software-scalability","How to Design Enterprise Software That Scales With Your Business",[188,849,850],{},[171,851,853],{"href":852},"/blog/low-code-vs-custom-development","Low-Code vs Custom Development: When Each Actually Makes Sense",[188,855,856],{},[171,857,859],{"href":858},"/blog/api-first-architecture","API-First Architecture: Building Software That Integrates by Default",{"title":213,"searchDepth":214,"depth":214,"links":861},[862,863,864,865,866,867,868,869,870],{"id":566,"depth":217,"text":567},{"id":582,"depth":217,"text":583},{"id":614,"depth":217,"text":615},{"id":683,"depth":217,"text":684},{"id":726,"depth":217,"text":727},{"id":760,"depth":217,"text":761},{"id":787,"depth":217,"text":788},{"id":815,"depth":217,"text":816},{"id":182,"depth":217,"text":183},"SaaS vs on-premise is not a technology decision — it's a business decision. Here's the framework for choosing the right deployment model for your enterprise software.",[873,874],"SaaS vs on-premise","enterprise software deployment",{},"/blog/saas-vs-on-premise",9,{"title":560,"description":871},"blog/saas-vs-on-premise",[881,239,882,883,884],"Enterprise Software","Deployment","Strategy","Architecture","6HL_7fdp64BbYcJGUJOXOubNn8dtodE6wwCH6wFaB2Y",{"id":887,"title":888,"author":889,"body":890,"category":223,"date":224,"description":1105,"extension":226,"featured":227,"image":228,"keywords":1106,"meta":1109,"navigation":233,"path":1110,"readTime":235,"seo":1111,"stem":1112,"tags":1113,"__hash__":1117},"blog/blog/scope-creep-prevention.md","Scope Creep Prevention: How to Keep Custom Software Projects on Track",{"name":9,"bio":10},{"type":12,"value":891,"toc":1095},[892,896,899,902,904,908,914,920,926,932,934,938,941,944,958,961,963,967,970,973,979,985,991,997,1003,1006,1008,1012,1015,1018,1020,1024,1027,1041,1044,1047,1049,1053,1056,1059,1062,1064,1071,1073,1075],[15,893,895],{"id":894},"scope-creep-is-a-relationship-problem","Scope Creep Is a Relationship Problem",[20,897,898],{},"Most people frame scope creep as a planning problem — you didn't define the requirements clearly enough, the spec wasn't detailed enough, the estimate was too rough. That's partly true. But scope creep is fundamentally a relationship problem. It happens when the boundaries between what was agreed and what's being requested aren't maintained, and when both parties have different assumptions about what \"the project\" includes.",[20,900,901],{},"Every software project I've run has had scope pressure. The question isn't whether your client will ask for more — they will. The question is whether you have the systems and the relationship quality to handle those requests without derailing the project.",[27,903],{},[15,905,907],{"id":906},"why-scope-creep-happens","Why Scope Creep Happens",[20,909,910,913],{},[36,911,912],{},"Requirements are genuinely ambiguous."," Clients often don't know exactly what they want until they see something working. \"A dashboard with key metrics\" sounds clear until the first prototype surfaces, and suddenly there are 12 metrics they didn't think of, a date filter that needs to work in a specific way, and a comparison view they absolutely need. This isn't dishonesty — it's the nature of discovery through concrete examples.",[20,915,916,919],{},[36,917,918],{},"Stakeholders have different visions."," On any project with multiple stakeholders, everyone has a mental model of the finished product. Those models don't fully align until they're forced to converge on something concrete. Every person whose vision isn't fully captured will try to add their piece.",[20,921,922,925],{},[36,923,924],{},"Easy additions seem low-cost."," \"Can you just add a CSV export? That can't be more than a day's work.\" Sometimes that's accurate. Often it isn't — the export needs to handle edge cases, be formatted correctly, respect permissions, include the right columns, and be tested. What looks small from the outside has development surface area that isn't visible.",[20,927,928,931],{},[36,929,930],{},"The original spec had gaps."," All specs have gaps. The question is when they surface — before development or during it. Gaps that surface during development become scope additions unless you have a process for handling them.",[27,933],{},[15,935,937],{"id":936},"the-foundation-a-tight-specification","The Foundation: A Tight Specification",[20,939,940],{},"The best defense against scope creep is a specification that is unambiguously clear about what's in and what's out. Not a high-level requirements document — a detailed functional specification that describes each feature's behavior with enough precision that a developer and a client reading it independently would have the same understanding.",[20,942,943],{},"For each feature, the spec should include:",[185,945,946,949,952,955],{},[188,947,948],{},"What the user does",[188,950,951],{},"What the system does in response",[188,953,954],{},"Edge cases and error states",[188,956,957],{},"What the feature explicitly does not include",[20,959,960],{},"That last item is the one most specs omit. Explicitly documenting what's out of scope is as important as documenting what's in scope. \"User authentication includes email/password login and password reset. Social login (Google, Apple) is out of scope for v1\" prevents the conversation six weeks later about why the client can't log in with Google.",[27,962],{},[15,964,966],{"id":965},"the-change-control-process","The Change Control Process",[20,968,969],{},"Every project needs a formal process for evaluating scope changes. Not a bureaucratic obstacle — a lightweight mechanism that makes the cost of changes visible and gives both parties a way to make explicit decisions about them.",[20,971,972],{},"Here's the process I use:",[20,974,975,978],{},[36,976,977],{},"Step 1: Log the request."," When a client asks for something that isn't in the spec, I acknowledge it and note it in writing. \"Got it — I'll add this to the change log and scope it out.\"",[20,980,981,984],{},[36,982,983],{},"Step 2: Evaluate the request."," How long will it take? Does it affect any existing work? Does it introduce dependencies? What's the impact on timeline and cost?",[20,986,987,990],{},[36,988,989],{},"Step 3: Present the impact."," \"Adding the multi-language support will add approximately 5 days of development and $3,500 to the project. This would push the launch date from March 15 to March 22.\" The client now has the information they need to make a decision.",[20,992,993,996],{},[36,994,995],{},"Step 4: Get written approval to proceed."," A reply email or a signed change order. Never just a verbal \"yeah, go ahead.\" Written confirmation creates a shared record.",[20,998,999,1002],{},[36,1000,1001],{},"Step 5: Update the spec and timeline."," Approved changes become part of the official scope. Everything else stays out.",[20,1004,1005],{},"This process protects both parties. The client can't claim they didn't know about the cost impact. The developer can't be accused of adding things the client didn't ask for.",[27,1007],{},[15,1009,1011],{"id":1010},"managing-the-while-youre-at-it-dynamic","Managing the \"While You're At It\" Dynamic",[20,1013,1014],{},"Every developer who has worked with clients knows this moment: you're deep in a feature, the client drops by (virtually or otherwise), and says \"while you're at it, can you also...\" This is one of the most common delivery vectors for scope creep.",[20,1016,1017],{},"The answer to \"while you're at it\" is not \"yes\" and it's not \"no.\" It's: \"I can scope that out — is this something we want to add to the project, or should it go in the backlog for a future phase?\" This redirects the conversation without creating conflict, keeps the current work on track, and preserves the addition as a potential future engagement.",[27,1019],{},[15,1021,1023],{"id":1022},"when-to-say-no-outright","When to Say No Outright",[20,1025,1026],{},"Some scope requests should be declined, or at minimum deferred. When a requested feature:",[185,1028,1029,1032,1035,1038],{},[188,1030,1031],{},"Would require rearchitecting a significant portion of work already done",[188,1033,1034],{},"Is in tension with a core design decision that was deliberate",[188,1036,1037],{},"Has unclear requirements that would take longer to define than to build",[188,1039,1040],{},"Represents a pivot in the product direction that warrants a separate project conversation",[20,1042,1043],{},"... The right answer is to pause the conversation and reframe it. \"This request is significant enough that I think it deserves its own conversation — let's schedule time to talk through what you're trying to accomplish and whether this is the right way to get there.\"",[20,1045,1046],{},"This is a service to the client. Scope that adds complexity without clear purpose is not a feature — it's technical debt masquerading as a feature.",[27,1048],{},[15,1050,1052],{"id":1051},"the-timeline-buffer-that-saves-projects","The Timeline Buffer That Saves Projects",[20,1054,1055],{},"No matter how tight the specification, add buffer to the schedule. For straightforward projects, 15-20% of the total timeline. For complex or novel projects, 25-35%. This buffer exists to absorb the inevitable — discovered edge cases, late-arriving decisions from the client, integration surprises with third-party systems, and yes, some legitimate scope additions.",[20,1057,1058],{},"When clients push back on buffer, I explain it this way: \"The buffer isn't for things going wrong. It's for the things we know we don't know yet. On every project, there are things we'll discover during development that we couldn't see during planning. The buffer is what keeps us from renegotiating the contract every time we discover one.\"",[20,1060,1061],{},"Buffer that isn't used at the end of a project is an on-time delivery. That's a better outcome than a no-buffer project that runs two weeks late because of a third-party API that behaved unexpectedly.",[27,1063],{},[20,1065,1066,1067,1070],{},"Scope creep prevention is an ops discipline, not a technical one. If you're running a software project and want help building the processes that keep it from going sideways, book a call at ",[171,1068,176],{"href":173,"rel":1069},[175]," — this is exactly the kind of problem I help clients solve.",[27,1072],{},[15,1074,183],{"id":182},[185,1076,1077,1081,1087,1091],{},[188,1078,1079],{},[171,1080,193],{"href":192},[188,1082,1083],{},[171,1084,1086],{"href":1085},"/blog/software-project-management-guide","Software Project Management for Non-Technical Founders",[188,1088,1089],{},[171,1090,205],{"href":204},[188,1092,1093],{},[171,1094,211],{"href":210},{"title":213,"searchDepth":214,"depth":214,"links":1096},[1097,1098,1099,1100,1101,1102,1103,1104],{"id":894,"depth":217,"text":895},{"id":906,"depth":217,"text":907},{"id":936,"depth":217,"text":937},{"id":965,"depth":217,"text":966},{"id":1010,"depth":217,"text":1011},{"id":1022,"depth":217,"text":1023},{"id":1051,"depth":217,"text":1052},{"id":182,"depth":217,"text":183},"Scope creep is the number one reason software projects run over budget and schedule. Here's the system for preventing it and handling it when it happens anyway.",[1107,1108],"scope creep prevention","software project management",{},"/blog/scope-creep-prevention",{"title":888,"description":1105},"blog/scope-creep-prevention",[1114,1115,1116],"Project Management","Scope Creep","Software Development","dMomXVHf3A6SibUOXOdMKpg9QaFlhtlgIQbxBPt6ETc",{"id":1119,"title":1120,"author":1121,"body":1122,"category":1983,"date":224,"description":1984,"extension":226,"featured":227,"image":228,"keywords":1985,"meta":1988,"navigation":233,"path":1989,"readTime":235,"seo":1990,"stem":1991,"tags":1992,"__hash__":1995},"blog/blog/secrets-management-guide.md","Secrets Management: Keeping Credentials Out of Your Codebase",{"name":9,"bio":10},{"type":12,"value":1123,"toc":1974},[1124,1128,1131,1134,1138,1144,1147,1150,1154,1165,1242,1249,1378,1381,1446,1449,1453,1458,1468,1471,1476,1479,1662,1665,1670,1673,1834,1841,1846,1849,1852,1856,1859,1865,1871,1877,1880,1884,1887,1890,1893,1896,1900,1903,1929,1932,1934,1940,1942,1944,1970],[1125,1126,1120],"h1",{"id":1127},"secrets-management-keeping-credentials-out-of-your-codebase",[20,1129,1130],{},"In 2026, there are still thousands of GitHub repositories with database passwords, API keys, and AWS access credentials committed in their history. Search GitHub for \"remove password\" in commit messages and you will find evidence of the cleanup that happens after these mistakes. But cleanup is not sufficient — once a secret is in your git history, it must be treated as compromised. Rotating it is mandatory.",[20,1132,1133],{},"The good news is that properly managed secrets are not complicated. The tools are mature, the patterns are well-established, and the operational overhead is minimal once you set it up correctly. Here is the complete guide.",[15,1135,1137],{"id":1136},"the-hard-rule-secrets-never-touch-your-repository","The Hard Rule: Secrets Never Touch Your Repository",[20,1139,1140,1141,1143],{},"I want to start with the absolute rule before getting into tooling: secrets never appear in your repository. Not in code, not in configuration files, not in comments, not in commit messages, not in ",[287,1142,329],{}," files that accidentally do not get gitignored. Not once, not even in a commit you plan to immediately revert.",[20,1145,1146],{},"The reason is git history is permanent. Reverting a commit does not remove it from history. Squashing commits does not remove it. Force-pushing to remove history can corrupt remote repository state and requires notifying everyone who has cloned the repository that they need to re-clone. Even after all that, the secret may have been indexed by GitHub's search, scraped by a bot, or cached somewhere.",[20,1148,1149],{},"The operationally correct response when a secret is committed is: assume it compromised, rotate it immediately, then clean the history. In that order. Rotate first because the rotation is urgent. History cleanup can happen after the immediate risk is addressed.",[15,1151,1153],{"id":1152},"detecting-secrets-before-they-are-committed","Detecting Secrets Before They Are Committed",[20,1155,1156,1157,1160,1161,1164],{},"Pre-commit hooks can catch secrets before they enter your repository. ",[287,1158,1159],{},"git-secrets"," and ",[287,1162,1163],{},"detect-secrets"," are two tools designed for this:",[378,1166,1170],{"className":1167,"code":1168,"language":1169,"meta":213,"style":213},"language-bash shiki shiki-themes github-dark","# Install detect-secrets\npip install detect-secrets\n\n# Initialize a baseline (mark known false positives as allowed)\ndetect-secrets scan > .secrets.baseline\n\n# Install the pre-commit hook\ndetect-secrets-hook --baseline .secrets.baseline\n","bash",[287,1171,1172,1181,1194,1199,1205,1220,1225,1230],{"__ignoreMap":213},[1173,1174,1177],"span",{"class":1175,"line":1176},"line",1,[1173,1178,1180],{"class":1179},"sAwPA","# Install detect-secrets\n",[1173,1182,1183,1187,1191],{"class":1175,"line":217},[1173,1184,1186],{"class":1185},"svObZ","pip",[1173,1188,1190],{"class":1189},"sU2Wk"," install",[1173,1192,1193],{"class":1189}," detect-secrets\n",[1173,1195,1196],{"class":1175,"line":214},[1173,1197,1198],{"emptyLinePlaceholder":233},"\n",[1173,1200,1202],{"class":1175,"line":1201},4,[1173,1203,1204],{"class":1179},"# Initialize a baseline (mark known false positives as allowed)\n",[1173,1206,1208,1210,1213,1217],{"class":1175,"line":1207},5,[1173,1209,1163],{"class":1185},[1173,1211,1212],{"class":1189}," scan",[1173,1214,1216],{"class":1215},"snl16"," >",[1173,1218,1219],{"class":1189}," .secrets.baseline\n",[1173,1221,1223],{"class":1175,"line":1222},6,[1173,1224,1198],{"emptyLinePlaceholder":233},[1173,1226,1227],{"class":1175,"line":235},[1173,1228,1229],{"class":1179},"# Install the pre-commit hook\n",[1173,1231,1233,1236,1240],{"class":1175,"line":1232},8,[1173,1234,1235],{"class":1185},"detect-secrets-hook",[1173,1237,1239],{"class":1238},"sDLfK"," --baseline",[1173,1241,1219],{"class":1189},[20,1243,1244,1245,1248],{},"Or using the ",[287,1246,1247],{},"pre-commit"," framework with multiple hooks:",[378,1250,1254],{"className":1251,"code":1252,"language":1253,"meta":213,"style":213},"language-yaml shiki shiki-themes github-dark","# .pre-commit-config.yaml\nrepos:\n - repo: https://github.com/Yelp/detect-secrets\n rev: v1.4.0\n hooks:\n - id: detect-secrets\n args: [\"--baseline\", \".secrets.baseline\"]\n\n - repo: https://github.com/awslabs/git-secrets\n rev: 1.3.0\n hooks:\n - id: git-secrets\n","yaml",[287,1255,1256,1261,1271,1285,1295,1302,1314,1334,1338,1349,1359,1366],{"__ignoreMap":213},[1173,1257,1258],{"class":1175,"line":1176},[1173,1259,1260],{"class":1179},"# .pre-commit-config.yaml\n",[1173,1262,1263,1267],{"class":1175,"line":217},[1173,1264,1266],{"class":1265},"s4JwU","repos",[1173,1268,1270],{"class":1269},"s95oV",":\n",[1173,1272,1273,1276,1279,1282],{"class":1175,"line":214},[1173,1274,1275],{"class":1269}," - ",[1173,1277,1278],{"class":1265},"repo",[1173,1280,1281],{"class":1269},": ",[1173,1283,1284],{"class":1189},"https://github.com/Yelp/detect-secrets\n",[1173,1286,1287,1290,1292],{"class":1175,"line":1201},[1173,1288,1289],{"class":1265}," rev",[1173,1291,1281],{"class":1269},[1173,1293,1294],{"class":1189},"v1.4.0\n",[1173,1296,1297,1300],{"class":1175,"line":1207},[1173,1298,1299],{"class":1265}," hooks",[1173,1301,1270],{"class":1269},[1173,1303,1304,1306,1309,1311],{"class":1175,"line":1222},[1173,1305,1275],{"class":1269},[1173,1307,1308],{"class":1265},"id",[1173,1310,1281],{"class":1269},[1173,1312,1313],{"class":1189},"detect-secrets\n",[1173,1315,1316,1319,1322,1325,1328,1331],{"class":1175,"line":235},[1173,1317,1318],{"class":1265}," args",[1173,1320,1321],{"class":1269},": [",[1173,1323,1324],{"class":1189},"\"--baseline\"",[1173,1326,1327],{"class":1269},", ",[1173,1329,1330],{"class":1189},"\".secrets.baseline\"",[1173,1332,1333],{"class":1269},"]\n",[1173,1335,1336],{"class":1175,"line":1232},[1173,1337,1198],{"emptyLinePlaceholder":233},[1173,1339,1340,1342,1344,1346],{"class":1175,"line":877},[1173,1341,1275],{"class":1269},[1173,1343,1278],{"class":1265},[1173,1345,1281],{"class":1269},[1173,1347,1348],{"class":1189},"https://github.com/awslabs/git-secrets\n",[1173,1350,1352,1354,1356],{"class":1175,"line":1351},10,[1173,1353,1289],{"class":1265},[1173,1355,1281],{"class":1269},[1173,1357,1358],{"class":1238},"1.3.0\n",[1173,1360,1362,1364],{"class":1175,"line":1361},11,[1173,1363,1299],{"class":1265},[1173,1365,1270],{"class":1269},[1173,1367,1369,1371,1373,1375],{"class":1175,"line":1368},12,[1173,1370,1275],{"class":1269},[1173,1372,1308],{"class":1265},[1173,1374,1281],{"class":1269},[1173,1376,1377],{"class":1189},"git-secrets\n",[20,1379,1380],{},"In CI, run the same scan on every PR:",[378,1382,1384],{"className":1251,"code":1383,"language":1253,"meta":213,"style":213},"- name: Scan for secrets\n uses: trufflesecurity/trufflehog@main\n with:\n path: ./\n base: ${{ github.event.repository.default_branch }}\n head: HEAD\n",[287,1385,1386,1399,1409,1416,1426,1436],{"__ignoreMap":213},[1173,1387,1388,1391,1394,1396],{"class":1175,"line":1176},[1173,1389,1390],{"class":1269},"- ",[1173,1392,1393],{"class":1265},"name",[1173,1395,1281],{"class":1269},[1173,1397,1398],{"class":1189},"Scan for secrets\n",[1173,1400,1401,1404,1406],{"class":1175,"line":217},[1173,1402,1403],{"class":1265}," uses",[1173,1405,1281],{"class":1269},[1173,1407,1408],{"class":1189},"trufflesecurity/trufflehog@main\n",[1173,1410,1411,1414],{"class":1175,"line":214},[1173,1412,1413],{"class":1265}," with",[1173,1415,1270],{"class":1269},[1173,1417,1418,1421,1423],{"class":1175,"line":1201},[1173,1419,1420],{"class":1265}," path",[1173,1422,1281],{"class":1269},[1173,1424,1425],{"class":1189},"./\n",[1173,1427,1428,1431,1433],{"class":1175,"line":1207},[1173,1429,1430],{"class":1265}," base",[1173,1432,1281],{"class":1269},[1173,1434,1435],{"class":1189},"${{ github.event.repository.default_branch }}\n",[1173,1437,1438,1441,1443],{"class":1175,"line":1222},[1173,1439,1440],{"class":1265}," head",[1173,1442,1281],{"class":1269},[1173,1444,1445],{"class":1189},"HEAD\n",[20,1447,1448],{},"TruffleHog is excellent for CI scanning — it scans the diff of a PR against the base branch and reports any high-entropy strings or credential patterns it finds.",[15,1450,1452],{"id":1451},"secrets-management-tools-by-scale","Secrets Management Tools by Scale",[20,1454,1455],{},[36,1456,1457],{},"For local development (any team size): Doppler",[20,1459,1460,1461,1464,1465,1467],{},"Doppler centralizes your secrets and injects them into running processes. Developers install the Doppler CLI, authenticate, and run ",[287,1462,1463],{},"doppler run -- npm run dev",". Doppler injects the configured environment variables at process startup. No ",[287,1466,329],{}," files, no manual credential sharing through Slack.",[20,1469,1470],{},"The free tier covers up to five projects and unlimited users. The UI is clean, the CLI is fast, and the integration with all major platforms (GitHub Actions, Vercel, Railway, Kubernetes) is excellent.",[20,1472,1473],{},[36,1474,1475],{},"For AWS-centric infrastructure: AWS Secrets Manager",[20,1477,1478],{},"AWS Secrets Manager stores secrets with automatic rotation, cross-region replication, and fine-grained IAM access control. Secrets are accessed via API — your application retrieves them at startup rather than having them injected as environment variables.",[378,1480,1484],{"className":1481,"code":1482,"language":1483,"meta":213,"style":213},"language-typescript shiki shiki-themes github-dark","import { SecretsManagerClient, GetSecretValueCommand } from \"@aws-sdk/client-secrets-manager\";\n\nAsync function getSecret(secretName: string): Promise\u003Cstring> {\n const client = new SecretsManagerClient({ region: \"us-east-1\" });\n const response = await client.send(\n new GetSecretValueCommand({ SecretId: secretName })\n );\n return response.SecretString ?? \"\";\n}\n\n// At application startup\nconst dbPassword = await getSecret(\"production/api/database-password\");\n","typescript",[287,1485,1486,1503,1507,1548,1574,1595,1605,1610,1626,1631,1635,1640],{"__ignoreMap":213},[1173,1487,1488,1491,1494,1497,1500],{"class":1175,"line":1176},[1173,1489,1490],{"class":1215},"import",[1173,1492,1493],{"class":1269}," { SecretsManagerClient, GetSecretValueCommand } ",[1173,1495,1496],{"class":1215},"from",[1173,1498,1499],{"class":1189}," \"@aws-sdk/client-secrets-manager\"",[1173,1501,1502],{"class":1269},";\n",[1173,1504,1505],{"class":1175,"line":217},[1173,1506,1198],{"emptyLinePlaceholder":233},[1173,1508,1509,1512,1515,1518,1521,1525,1528,1531,1534,1536,1539,1542,1545],{"class":1175,"line":214},[1173,1510,1511],{"class":1269},"Async ",[1173,1513,1514],{"class":1215},"function",[1173,1516,1517],{"class":1185}," getSecret",[1173,1519,1520],{"class":1269},"(",[1173,1522,1524],{"class":1523},"s9osk","secretName",[1173,1526,1527],{"class":1215},":",[1173,1529,1530],{"class":1238}," string",[1173,1532,1533],{"class":1269},")",[1173,1535,1527],{"class":1215},[1173,1537,1538],{"class":1185}," Promise",[1173,1540,1541],{"class":1269},"\u003C",[1173,1543,1544],{"class":1238},"string",[1173,1546,1547],{"class":1269},"> {\n",[1173,1549,1550,1553,1556,1559,1562,1565,1568,1571],{"class":1175,"line":1201},[1173,1551,1552],{"class":1215}," const",[1173,1554,1555],{"class":1238}," client",[1173,1557,1558],{"class":1215}," =",[1173,1560,1561],{"class":1215}," new",[1173,1563,1564],{"class":1185}," SecretsManagerClient",[1173,1566,1567],{"class":1269},"({ region: ",[1173,1569,1570],{"class":1189},"\"us-east-1\"",[1173,1572,1573],{"class":1269}," });\n",[1173,1575,1576,1578,1581,1583,1586,1589,1592],{"class":1175,"line":1207},[1173,1577,1552],{"class":1215},[1173,1579,1580],{"class":1238}," response",[1173,1582,1558],{"class":1215},[1173,1584,1585],{"class":1215}," await",[1173,1587,1588],{"class":1269}," client.",[1173,1590,1591],{"class":1185},"send",[1173,1593,1594],{"class":1269},"(\n",[1173,1596,1597,1599,1602],{"class":1175,"line":1222},[1173,1598,1561],{"class":1215},[1173,1600,1601],{"class":1185}," GetSecretValueCommand",[1173,1603,1604],{"class":1269},"({ SecretId: secretName })\n",[1173,1606,1607],{"class":1175,"line":235},[1173,1608,1609],{"class":1269}," );\n",[1173,1611,1612,1615,1618,1621,1624],{"class":1175,"line":1232},[1173,1613,1614],{"class":1215}," return",[1173,1616,1617],{"class":1269}," response.SecretString ",[1173,1619,1620],{"class":1215},"??",[1173,1622,1623],{"class":1189}," \"\"",[1173,1625,1502],{"class":1269},[1173,1627,1628],{"class":1175,"line":877},[1173,1629,1630],{"class":1269},"}\n",[1173,1632,1633],{"class":1175,"line":1351},[1173,1634,1198],{"emptyLinePlaceholder":233},[1173,1636,1637],{"class":1175,"line":1361},[1173,1638,1639],{"class":1179},"// At application startup\n",[1173,1641,1642,1645,1648,1650,1652,1654,1656,1659],{"class":1175,"line":1368},[1173,1643,1644],{"class":1215},"const",[1173,1646,1647],{"class":1238}," dbPassword",[1173,1649,1558],{"class":1215},[1173,1651,1585],{"class":1215},[1173,1653,1517],{"class":1185},[1173,1655,1520],{"class":1269},[1173,1657,1658],{"class":1189},"\"production/api/database-password\"",[1173,1660,1661],{"class":1269},");\n",[20,1663,1664],{},"The advantage over environment variables is that secrets can be rotated without restarting the application if you re-fetch them periodically. The disadvantage is coupling your application to AWS SDK for configuration.",[20,1666,1667],{},[36,1668,1669],{},"For Kubernetes: External Secrets Operator",[20,1671,1672],{},"The External Secrets Operator synchronizes secrets from external secret management systems (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) into Kubernetes Secrets. Your application references Kubernetes Secrets as normal — no external SDK calls, no cloud provider coupling:",[378,1674,1676],{"className":1251,"code":1675,"language":1253,"meta":213,"style":213},"apiVersion: external-secrets.io/v1beta1\nkind: ExternalSecret\nmetadata:\n name: api-secrets\n namespace: production\nspec:\n refreshInterval: 1h\n secretStoreRef:\n name: aws-secrets-manager\n kind: SecretStore\n target:\n name: api-secrets\n creationPolicy: Owner\n data:\n - secretKey: database-url\n remoteRef:\n key: production/api/database-url\n",[287,1677,1678,1688,1698,1705,1715,1725,1732,1742,1749,1758,1768,1775,1783,1794,1802,1815,1823],{"__ignoreMap":213},[1173,1679,1680,1683,1685],{"class":1175,"line":1176},[1173,1681,1682],{"class":1265},"apiVersion",[1173,1684,1281],{"class":1269},[1173,1686,1687],{"class":1189},"external-secrets.io/v1beta1\n",[1173,1689,1690,1693,1695],{"class":1175,"line":217},[1173,1691,1692],{"class":1265},"kind",[1173,1694,1281],{"class":1269},[1173,1696,1697],{"class":1189},"ExternalSecret\n",[1173,1699,1700,1703],{"class":1175,"line":214},[1173,1701,1702],{"class":1265},"metadata",[1173,1704,1270],{"class":1269},[1173,1706,1707,1710,1712],{"class":1175,"line":1201},[1173,1708,1709],{"class":1265}," name",[1173,1711,1281],{"class":1269},[1173,1713,1714],{"class":1189},"api-secrets\n",[1173,1716,1717,1720,1722],{"class":1175,"line":1207},[1173,1718,1719],{"class":1265}," namespace",[1173,1721,1281],{"class":1269},[1173,1723,1724],{"class":1189},"production\n",[1173,1726,1727,1730],{"class":1175,"line":1222},[1173,1728,1729],{"class":1265},"spec",[1173,1731,1270],{"class":1269},[1173,1733,1734,1737,1739],{"class":1175,"line":235},[1173,1735,1736],{"class":1265}," refreshInterval",[1173,1738,1281],{"class":1269},[1173,1740,1741],{"class":1189},"1h\n",[1173,1743,1744,1747],{"class":1175,"line":1232},[1173,1745,1746],{"class":1265}," secretStoreRef",[1173,1748,1270],{"class":1269},[1173,1750,1751,1753,1755],{"class":1175,"line":877},[1173,1752,1709],{"class":1265},[1173,1754,1281],{"class":1269},[1173,1756,1757],{"class":1189},"aws-secrets-manager\n",[1173,1759,1760,1763,1765],{"class":1175,"line":1351},[1173,1761,1762],{"class":1265}," kind",[1173,1764,1281],{"class":1269},[1173,1766,1767],{"class":1189},"SecretStore\n",[1173,1769,1770,1773],{"class":1175,"line":1361},[1173,1771,1772],{"class":1265}," target",[1173,1774,1270],{"class":1269},[1173,1776,1777,1779,1781],{"class":1175,"line":1368},[1173,1778,1709],{"class":1265},[1173,1780,1281],{"class":1269},[1173,1782,1714],{"class":1189},[1173,1784,1786,1789,1791],{"class":1175,"line":1785},13,[1173,1787,1788],{"class":1265}," creationPolicy",[1173,1790,1281],{"class":1269},[1173,1792,1793],{"class":1189},"Owner\n",[1173,1795,1797,1800],{"class":1175,"line":1796},14,[1173,1798,1799],{"class":1265}," data",[1173,1801,1270],{"class":1269},[1173,1803,1805,1807,1810,1812],{"class":1175,"line":1804},15,[1173,1806,1275],{"class":1269},[1173,1808,1809],{"class":1265},"secretKey",[1173,1811,1281],{"class":1269},[1173,1813,1814],{"class":1189},"database-url\n",[1173,1816,1818,1821],{"class":1175,"line":1817},16,[1173,1819,1820],{"class":1265}," remoteRef",[1173,1822,1270],{"class":1269},[1173,1824,1826,1829,1831],{"class":1175,"line":1825},17,[1173,1827,1828],{"class":1265}," key",[1173,1830,1281],{"class":1269},[1173,1832,1833],{"class":1189},"production/api/database-url\n",[20,1835,1836,1837,1840],{},"This creates a Kubernetes Secret named ",[287,1838,1839],{},"api-secrets"," that is refreshed hourly from AWS Secrets Manager. When the secret rotates in Secrets Manager, the Kubernetes Secret updates automatically.",[20,1842,1843],{},[36,1844,1845],{},"For self-hosted or multi-cloud: HashiCorp Vault",[20,1847,1848],{},"Vault is the most comprehensive solution: dynamic secrets (generate credentials on demand, automatically revoke them when done), detailed audit logging, fine-grained access control policies, and support for dozens of secret backends. It is also significantly more complex to operate.",[20,1850,1851],{},"For teams with the operational capacity, Vault is the gold standard. For most small to mid-sized teams, Doppler or AWS Secrets Manager provides 90% of the value with a fraction of the operational overhead.",[15,1853,1855],{"id":1854},"secret-injection-patterns","Secret Injection Patterns",[20,1857,1858],{},"The three patterns for getting secrets into your application:",[20,1860,1861,1864],{},[36,1862,1863],{},"Environment variable injection"," — your secrets manager injects secrets as environment variables at process startup. Simple, universal, but secrets are visible to any process in the same environment and are captured in crash dumps.",[20,1866,1867,1870],{},[36,1868,1869],{},"File mounting"," — secrets are written to a file at a well-known path. The application reads the file. Files can have strict permissions (readable only by the application user). Kubernetes Secrets can be mounted as files using volume mounts.",[20,1872,1873,1876],{},[36,1874,1875],{},"Direct API access"," — the application calls the secrets manager API to retrieve secrets at startup. The most flexible and most complex. Appropriate when you need dynamic secrets or per-request credential generation.",[20,1878,1879],{},"For most applications, environment variable injection is the pragmatic choice. It works with every framework, requires no application code changes, and is supported by every secrets management tool.",[15,1881,1883],{"id":1882},"audit-trails","Audit Trails",[20,1885,1886],{},"Know who accessed what secret when. Every secrets management tool provides access logging. Review it.",[20,1888,1889],{},"AWS Secrets Manager access is logged in CloudTrail. Enable CloudTrail if you have not — it logs all API calls in your AWS account. Set up an alert for unusual access patterns: a secret being accessed from a new IP, a secret being accessed at unusual times, or a secret being accessed by an unexpected IAM principal.",[20,1891,1892],{},"Doppler provides an access audit log per secret. When a secret is retrieved or updated, it is recorded with the accessor and timestamp.",[20,1894,1895],{},"Audit logs are useful in two scenarios: detecting unauthorized access to secrets (security monitoring) and attributing changes when something breaks (troubleshooting \"who changed the database password?\").",[15,1897,1899],{"id":1898},"the-secrets-hygiene-checklist","The Secrets Hygiene Checklist",[20,1901,1902],{},"Every production application should satisfy:",[185,1904,1905,1908,1911,1914,1917,1920,1923,1926],{},[188,1906,1907],{},"No secrets in the git repository or history",[188,1909,1910],{},"Secrets scanning in pre-commit hooks and CI",[188,1912,1913],{},"All production secrets stored in a dedicated secrets manager",[188,1915,1916],{},"Secrets scoped to the minimum necessary access (the database password for the API does not need admin privileges)",[188,1918,1919],{},"Access to production secrets limited to production systems and authorized team members",[188,1921,1922],{},"Audit logging enabled and reviewed",[188,1924,1925],{},"Rotation schedule defined for each secret",[188,1927,1928],{},"Automated rotation configured where the secret manager and service support it",[20,1930,1931],{},"This is not a complex or expensive checklist. The tooling exists, most of it has free tiers, and the setup time is measured in hours, not days.",[27,1933],{},[20,1935,1936,1937,177],{},"If you need help implementing secrets management for your team or want to audit your current credential handling practices, book a session at ",[171,1938,173],{"href":173,"rel":1939},[175],[27,1941],{},[15,1943,183],{"id":182},[185,1945,1946,1952,1958,1964],{},[188,1947,1948],{},[171,1949,1951],{"href":1950},"/blog/container-security-guide","Container Security: Hardening Docker for Production",[188,1953,1954],{},[171,1955,1957],{"href":1956},"/blog/environment-variables-guide","Environment Variables Done Right: Secrets, Config, and Everything In Between",[188,1959,1960],{},[171,1961,1963],{"href":1962},"/blog/performance-monitoring-guide","Application Performance Monitoring: Beyond the Health Check Endpoint",[188,1965,1966],{},[171,1967,1969],{"href":1968},"/blog/cdn-configuration-guide","CDN Configuration: Making Your Static Assets Load Instantly Everywhere",[1971,1972,1973],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":214,"depth":214,"links":1975},[1976,1977,1978,1979,1980,1981,1982],{"id":1136,"depth":217,"text":1137},{"id":1152,"depth":217,"text":1153},{"id":1451,"depth":217,"text":1452},{"id":1854,"depth":217,"text":1855},{"id":1882,"depth":217,"text":1883},{"id":1898,"depth":217,"text":1899},{"id":182,"depth":217,"text":183},"DevOps","A practical guide to secrets management for development teams — vault solutions, secret injection patterns, rotation automation, and audit trails for production credentials.",[1986,1987],"secrets management","environment security",{},"/blog/secrets-management-guide",{"title":1120,"description":1984},"blog/secrets-management-guide",[1993,555,1983,1994],"Secrets Management","Credentials","gux1jwX3y3jf629dz-fQtu-3tao-TnlLy1CtMei2-eg",{"id":1997,"title":1998,"author":1999,"body":2000,"category":555,"date":224,"description":3416,"extension":226,"featured":227,"image":228,"keywords":3417,"meta":3420,"navigation":233,"path":3421,"readTime":235,"seo":3422,"stem":3423,"tags":3424,"__hash__":3427},"blog/blog/secrets-rotation-guide.md","Secrets Rotation: Why Rotating Credentials Should Be Automatic",{"name":9,"bio":10},{"type":12,"value":2001,"toc":3407},[2002,2005,2008,2011,2014,2018,2024,2030,2036,2042,2048,2054,2058,2061,2064,2069,2087,2090,2093,2156,2159,2163,2166,2169,2794,2797,2820,2824,2827,3172,3175,3179,3182,3342,3345,3349,3352,3363,3366,3368,3374,3376,3378,3404],[1125,2003,1998],{"id":2004},"secrets-rotation-why-rotating-credentials-should-be-automatic",[20,2006,2007],{},"A credential that never changes is a credential that, once compromised, stays compromised indefinitely. The attacker who obtained your database password six months ago can still access your database today, if you have not rotated it. The API key leaked in a git commit three years ago — before you caught it — may still be valid. The former employee who memorized the staging database password can still use it.",[20,2009,2010],{},"Secrets rotation limits the window of exposure. If a credential is compromised and you rotate every 90 days, the maximum exposure window is 90 days, not infinite. If you rotate on a 30-day schedule, it is 30 days. If you have automated rotation with fresh credentials every day, it is 24 hours.",[20,2012,2013],{},"The catch is that manual rotation is operationally painful enough that it does not happen consistently. You write it down as something to do quarterly, the quarter ends, you put it on next quarter's list, and two years later the credentials have never changed. Automation solves this.",[15,2015,2017],{"id":2016},"what-gets-rotated-and-how-often","What Gets Rotated and How Often",[20,2019,2020,2023],{},[36,2021,2022],{},"Database passwords:"," rotate quarterly minimum. Monthly for sensitive systems. AWS RDS, Supabase, and most managed database providers have mechanisms for rotation without taking the database offline.",[20,2025,2026,2029],{},[36,2027,2028],{},"JWT signing secrets:"," rotate annually or whenever team membership changes significantly. Rotation invalidates existing tokens — plan for a graceful transition period where both old and new signing keys are valid, allowing active sessions to transition naturally.",[20,2031,2032,2035],{},[36,2033,2034],{},"API keys for third-party services:"," rotate when team members with access leave, when you suspect compromise, and annually as a baseline. Use service accounts with limited permissions rather than personal API keys — service account keys can be rotated without affecting personal access.",[20,2037,2038,2041],{},[36,2039,2040],{},"Internal service-to-service credentials:"," rotate frequently, ideally with short-lived credentials generated on demand. AWS IAM roles with instance profiles are better than long-lived access keys because they rotate automatically.",[20,2043,2044,2047],{},[36,2045,2046],{},"TLS certificates:"," Let's Encrypt certificates expire every 90 days. Automate renewal via Certbot or your platform's certificate management. Set up monitoring that alerts 14 days before expiry as a backup to catch any automation failure.",[20,2049,2050,2053],{},[36,2051,2052],{},"Encryption keys:"," key rotation for data encryption is complex because rotating the key requires re-encrypting all encrypted data. Implement key rotation through envelope encryption — rotate the key encryption key (KEK) without re-encrypting the data encryption keys immediately. Build a background re-encryption job that gradually migrates to the new KEK.",[15,2055,2057],{"id":2056},"rotating-database-passwords-without-downtime","Rotating Database Passwords Without Downtime",[20,2059,2060],{},"The challenge with database password rotation is that your application needs to connect to the database. Changing the password while the application is running causes connection failures until you restart the application with the new password. On a single-instance deployment, there is a brief outage. On a multi-instance deployment, instances get inconsistent credentials.",[20,2062,2063],{},"The solution is a rotation strategy that maintains both old and new credentials briefly:",[20,2065,2066],{},[36,2067,2068],{},"Two-phase rotation:",[2070,2071,2072,2075,2078,2081,2084],"ol",{},[188,2073,2074],{},"Generate a new password",[188,2076,2077],{},"Add the new password as an alternative authentication credential for the database user",[188,2079,2080],{},"Update your secrets manager with the new password",[188,2082,2083],{},"Deploy/restart your application so it picks up the new password",[188,2085,2086],{},"Remove the old password from the database user",[20,2088,2089],{},"This means the database accepts both passwords during the transition window. No connection failures during rotation.",[20,2091,2092],{},"AWS Secrets Manager automates this for RDS databases with Lambda rotation functions:",[378,2094,2098],{"className":2095,"code":2096,"language":2097,"meta":213,"style":213},"language-json shiki shiki-themes github-dark","{\n \"SecretId\": \"production/api/database-password\",\n \"RotationLambdaARN\": \"arn:aws:lambda:us-east-1:123456789:function:SecretsManagerRDSRotation\",\n \"RotationRules\": {\n \"AutomaticallyAfterDays\": 30\n }\n}\n","json",[287,2099,2100,2105,2117,2129,2137,2147,2152],{"__ignoreMap":213},[1173,2101,2102],{"class":1175,"line":1176},[1173,2103,2104],{"class":1269},"{\n",[1173,2106,2107,2110,2112,2114],{"class":1175,"line":217},[1173,2108,2109],{"class":1238}," \"SecretId\"",[1173,2111,1281],{"class":1269},[1173,2113,1658],{"class":1189},[1173,2115,2116],{"class":1269},",\n",[1173,2118,2119,2122,2124,2127],{"class":1175,"line":214},[1173,2120,2121],{"class":1238}," \"RotationLambdaARN\"",[1173,2123,1281],{"class":1269},[1173,2125,2126],{"class":1189},"\"arn:aws:lambda:us-east-1:123456789:function:SecretsManagerRDSRotation\"",[1173,2128,2116],{"class":1269},[1173,2130,2131,2134],{"class":1175,"line":1201},[1173,2132,2133],{"class":1238}," \"RotationRules\"",[1173,2135,2136],{"class":1269},": {\n",[1173,2138,2139,2142,2144],{"class":1175,"line":1207},[1173,2140,2141],{"class":1238}," \"AutomaticallyAfterDays\"",[1173,2143,1281],{"class":1269},[1173,2145,2146],{"class":1238},"30\n",[1173,2148,2149],{"class":1175,"line":1222},[1173,2150,2151],{"class":1269}," }\n",[1173,2153,2154],{"class":1175,"line":235},[1173,2155,1630],{"class":1269},[20,2157,2158],{},"The Lambda function handles the two-phase rotation automatically. Your application uses Secrets Manager's SDK to fetch the current secret — Secrets Manager transparently serves the current valid credential.",[15,2160,2162],{"id":2161},"rotating-jwt-signing-secrets","Rotating JWT Signing Secrets",[20,2164,2165],{},"JWT rotation requires careful handling because existing tokens are signed with the old secret. If you immediately invalidate the old secret, every user is logged out and must re-authenticate.",[20,2167,2168],{},"Graceful JWT rotation uses a key identifier (kid) claim to allow multiple valid signing keys simultaneously:",[378,2170,2172],{"className":1481,"code":2171,"language":1483,"meta":213,"style":213},"interface KeyPair {\n kid: string;\n secret: string;\n createdAt: Date;\n expiresAt: Date;\n}\n\nClass JwtKeyManager {\n private keys: Map\u003Cstring, KeyPair> = new Map();\n\n constructor() {\n this.loadKeys();\n }\n\n private loadKeys() {\n // Load current and previous key from secrets manager\n const keys = JSON.parse(process.env.JWT_KEYS!);\n keys.forEach((key: KeyPair) => this.keys.set(key.kid, key));\n }\n\n sign(payload: object): string {\n const currentKey = this.getCurrentKey();\n return jwt.sign(payload, currentKey.secret, {\n algorithm: \"HS256\",\n keyid: currentKey.kid,\n expiresIn: \"15m\",\n });\n }\n\n verify(token: string): jwt.JwtPayload {\n const decoded = jwt.decode(token, { complete: true });\n if (!decoded || typeof decoded === \"string\") {\n throw new Error(\"Invalid token\");\n }\n\n const kid = decoded.header.kid as string;\n const key = this.keys.get(kid);\n\n if (!key || new Date() > key.expiresAt) {\n throw new Error(\"Invalid or expired signing key\");\n }\n\n return jwt.verify(token, key.secret) as jwt.JwtPayload;\n }\n\n private getCurrentKey(): KeyPair {\n // Return the newest non-expired key\n return Array.from(this.keys.values())\n .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];\n }\n}\n",[287,2173,2174,2185,2196,2207,2219,2230,2234,2238,2243,2272,2276,2284,2296,2300,2304,2312,2317,2345,2381,2386,2391,2400,2419,2433,2444,2450,2461,2466,2471,2476,2485,2508,2540,2558,2563,2568,2587,2606,2611,2637,2653,2658,2663,2688,2693,2698,2708,2714,2737,2784,2789],{"__ignoreMap":213},[1173,2175,2176,2179,2182],{"class":1175,"line":1176},[1173,2177,2178],{"class":1215},"interface",[1173,2180,2181],{"class":1185}," KeyPair",[1173,2183,2184],{"class":1269}," {\n",[1173,2186,2187,2190,2192,2194],{"class":1175,"line":217},[1173,2188,2189],{"class":1523}," kid",[1173,2191,1527],{"class":1215},[1173,2193,1530],{"class":1238},[1173,2195,1502],{"class":1269},[1173,2197,2198,2201,2203,2205],{"class":1175,"line":214},[1173,2199,2200],{"class":1523}," secret",[1173,2202,1527],{"class":1215},[1173,2204,1530],{"class":1238},[1173,2206,1502],{"class":1269},[1173,2208,2209,2212,2214,2217],{"class":1175,"line":1201},[1173,2210,2211],{"class":1523}," createdAt",[1173,2213,1527],{"class":1215},[1173,2215,2216],{"class":1185}," Date",[1173,2218,1502],{"class":1269},[1173,2220,2221,2224,2226,2228],{"class":1175,"line":1207},[1173,2222,2223],{"class":1523}," expiresAt",[1173,2225,1527],{"class":1215},[1173,2227,2216],{"class":1185},[1173,2229,1502],{"class":1269},[1173,2231,2232],{"class":1175,"line":1222},[1173,2233,1630],{"class":1269},[1173,2235,2236],{"class":1175,"line":235},[1173,2237,1198],{"emptyLinePlaceholder":233},[1173,2239,2240],{"class":1175,"line":1232},[1173,2241,2242],{"class":1269},"Class JwtKeyManager {\n",[1173,2244,2245,2248,2251,2254,2256,2259,2262,2264,2266,2269],{"class":1175,"line":877},[1173,2246,2247],{"class":1269}," private ",[1173,2249,2250],{"class":1185},"keys",[1173,2252,2253],{"class":1269},": Map",[1173,2255,1541],{"class":1215},[1173,2257,2258],{"class":1269},"string, KeyPair",[1173,2260,2261],{"class":1215},">",[1173,2263,1558],{"class":1215},[1173,2265,1561],{"class":1215},[1173,2267,2268],{"class":1185}," Map",[1173,2270,2271],{"class":1269},"();\n",[1173,2273,2274],{"class":1175,"line":1351},[1173,2275,1198],{"emptyLinePlaceholder":233},[1173,2277,2278,2281],{"class":1175,"line":1361},[1173,2279,2280],{"class":1185}," constructor",[1173,2282,2283],{"class":1269},"() {\n",[1173,2285,2286,2289,2291,2294],{"class":1175,"line":1368},[1173,2287,2288],{"class":1238}," this",[1173,2290,177],{"class":1269},[1173,2292,2293],{"class":1185},"loadKeys",[1173,2295,2271],{"class":1269},[1173,2297,2298],{"class":1175,"line":1785},[1173,2299,2151],{"class":1269},[1173,2301,2302],{"class":1175,"line":1796},[1173,2303,1198],{"emptyLinePlaceholder":233},[1173,2305,2306,2308,2310],{"class":1175,"line":1804},[1173,2307,2247],{"class":1269},[1173,2309,2293],{"class":1185},[1173,2311,2283],{"class":1269},[1173,2313,2314],{"class":1175,"line":1817},[1173,2315,2316],{"class":1179}," // Load current and previous key from secrets manager\n",[1173,2318,2319,2321,2324,2326,2329,2331,2334,2337,2340,2343],{"class":1175,"line":1825},[1173,2320,1552],{"class":1215},[1173,2322,2323],{"class":1238}," keys",[1173,2325,1558],{"class":1215},[1173,2327,2328],{"class":1238}," JSON",[1173,2330,177],{"class":1269},[1173,2332,2333],{"class":1185},"parse",[1173,2335,2336],{"class":1269},"(process.env.",[1173,2338,2339],{"class":1238},"JWT_KEYS",[1173,2341,2342],{"class":1215},"!",[1173,2344,1661],{"class":1269},[1173,2346,2348,2351,2354,2357,2360,2362,2364,2367,2370,2372,2375,2378],{"class":1175,"line":2347},18,[1173,2349,2350],{"class":1269}," keys.",[1173,2352,2353],{"class":1185},"forEach",[1173,2355,2356],{"class":1269},"((",[1173,2358,2359],{"class":1523},"key",[1173,2361,1527],{"class":1215},[1173,2363,2181],{"class":1185},[1173,2365,2366],{"class":1269},") ",[1173,2368,2369],{"class":1215},"=>",[1173,2371,2288],{"class":1238},[1173,2373,2374],{"class":1269},".keys.",[1173,2376,2377],{"class":1185},"set",[1173,2379,2380],{"class":1269},"(key.kid, key));\n",[1173,2382,2384],{"class":1175,"line":2383},19,[1173,2385,2151],{"class":1269},[1173,2387,2389],{"class":1175,"line":2388},20,[1173,2390,1198],{"emptyLinePlaceholder":233},[1173,2392,2394,2397],{"class":1175,"line":2393},21,[1173,2395,2396],{"class":1185}," sign",[1173,2398,2399],{"class":1269},"(payload: object): string {\n",[1173,2401,2403,2405,2408,2410,2412,2414,2417],{"class":1175,"line":2402},22,[1173,2404,1552],{"class":1215},[1173,2406,2407],{"class":1238}," currentKey",[1173,2409,1558],{"class":1215},[1173,2411,2288],{"class":1238},[1173,2413,177],{"class":1269},[1173,2415,2416],{"class":1185},"getCurrentKey",[1173,2418,2271],{"class":1269},[1173,2420,2422,2424,2427,2430],{"class":1175,"line":2421},23,[1173,2423,1614],{"class":1215},[1173,2425,2426],{"class":1269}," jwt.",[1173,2428,2429],{"class":1185},"sign",[1173,2431,2432],{"class":1269},"(payload, currentKey.secret, {\n",[1173,2434,2436,2439,2442],{"class":1175,"line":2435},24,[1173,2437,2438],{"class":1269}," algorithm: ",[1173,2440,2441],{"class":1189},"\"HS256\"",[1173,2443,2116],{"class":1269},[1173,2445,2447],{"class":1175,"line":2446},25,[1173,2448,2449],{"class":1269}," keyid: currentKey.kid,\n",[1173,2451,2453,2456,2459],{"class":1175,"line":2452},26,[1173,2454,2455],{"class":1269}," expiresIn: ",[1173,2457,2458],{"class":1189},"\"15m\"",[1173,2460,2116],{"class":1269},[1173,2462,2464],{"class":1175,"line":2463},27,[1173,2465,1573],{"class":1269},[1173,2467,2469],{"class":1175,"line":2468},28,[1173,2470,2151],{"class":1269},[1173,2472,2474],{"class":1175,"line":2473},29,[1173,2475,1198],{"emptyLinePlaceholder":233},[1173,2477,2479,2482],{"class":1175,"line":2478},30,[1173,2480,2481],{"class":1185}," verify",[1173,2483,2484],{"class":1269},"(token: string): jwt.JwtPayload {\n",[1173,2486,2488,2490,2493,2495,2497,2500,2503,2506],{"class":1175,"line":2487},31,[1173,2489,1552],{"class":1215},[1173,2491,2492],{"class":1238}," decoded",[1173,2494,1558],{"class":1215},[1173,2496,2426],{"class":1269},[1173,2498,2499],{"class":1185},"decode",[1173,2501,2502],{"class":1269},"(token, { complete: ",[1173,2504,2505],{"class":1238},"true",[1173,2507,1573],{"class":1269},[1173,2509,2511,2514,2517,2519,2522,2525,2528,2531,2534,2537],{"class":1175,"line":2510},32,[1173,2512,2513],{"class":1215}," if",[1173,2515,2516],{"class":1269}," (",[1173,2518,2342],{"class":1215},[1173,2520,2521],{"class":1269},"decoded ",[1173,2523,2524],{"class":1215},"||",[1173,2526,2527],{"class":1215}," typeof",[1173,2529,2530],{"class":1269}," decoded ",[1173,2532,2533],{"class":1215},"===",[1173,2535,2536],{"class":1189}," \"string\"",[1173,2538,2539],{"class":1269},") {\n",[1173,2541,2543,2546,2548,2551,2553,2556],{"class":1175,"line":2542},33,[1173,2544,2545],{"class":1215}," throw",[1173,2547,1561],{"class":1215},[1173,2549,2550],{"class":1185}," Error",[1173,2552,1520],{"class":1269},[1173,2554,2555],{"class":1189},"\"Invalid token\"",[1173,2557,1661],{"class":1269},[1173,2559,2561],{"class":1175,"line":2560},34,[1173,2562,2151],{"class":1269},[1173,2564,2566],{"class":1175,"line":2565},35,[1173,2567,1198],{"emptyLinePlaceholder":233},[1173,2569,2571,2573,2575,2577,2580,2583,2585],{"class":1175,"line":2570},36,[1173,2572,1552],{"class":1215},[1173,2574,2189],{"class":1238},[1173,2576,1558],{"class":1215},[1173,2578,2579],{"class":1269}," decoded.header.kid ",[1173,2581,2582],{"class":1215},"as",[1173,2584,1530],{"class":1238},[1173,2586,1502],{"class":1269},[1173,2588,2590,2592,2594,2596,2598,2600,2603],{"class":1175,"line":2589},37,[1173,2591,1552],{"class":1215},[1173,2593,1828],{"class":1238},[1173,2595,1558],{"class":1215},[1173,2597,2288],{"class":1238},[1173,2599,2374],{"class":1269},[1173,2601,2602],{"class":1185},"get",[1173,2604,2605],{"class":1269},"(kid);\n",[1173,2607,2609],{"class":1175,"line":2608},38,[1173,2610,1198],{"emptyLinePlaceholder":233},[1173,2612,2614,2616,2618,2620,2623,2625,2627,2629,2632,2634],{"class":1175,"line":2613},39,[1173,2615,2513],{"class":1215},[1173,2617,2516],{"class":1269},[1173,2619,2342],{"class":1215},[1173,2621,2622],{"class":1269},"key ",[1173,2624,2524],{"class":1215},[1173,2626,1561],{"class":1215},[1173,2628,2216],{"class":1185},[1173,2630,2631],{"class":1269},"() ",[1173,2633,2261],{"class":1215},[1173,2635,2636],{"class":1269}," key.expiresAt) {\n",[1173,2638,2640,2642,2644,2646,2648,2651],{"class":1175,"line":2639},40,[1173,2641,2545],{"class":1215},[1173,2643,1561],{"class":1215},[1173,2645,2550],{"class":1185},[1173,2647,1520],{"class":1269},[1173,2649,2650],{"class":1189},"\"Invalid or expired signing key\"",[1173,2652,1661],{"class":1269},[1173,2654,2656],{"class":1175,"line":2655},41,[1173,2657,2151],{"class":1269},[1173,2659,2661],{"class":1175,"line":2660},42,[1173,2662,1198],{"emptyLinePlaceholder":233},[1173,2664,2666,2668,2670,2673,2676,2678,2681,2683,2686],{"class":1175,"line":2665},43,[1173,2667,1614],{"class":1215},[1173,2669,2426],{"class":1269},[1173,2671,2672],{"class":1185},"verify",[1173,2674,2675],{"class":1269},"(token, key.secret) ",[1173,2677,2582],{"class":1215},[1173,2679,2680],{"class":1185}," jwt",[1173,2682,177],{"class":1269},[1173,2684,2685],{"class":1185},"JwtPayload",[1173,2687,1502],{"class":1269},[1173,2689,2691],{"class":1175,"line":2690},44,[1173,2692,2151],{"class":1269},[1173,2694,2696],{"class":1175,"line":2695},45,[1173,2697,1198],{"emptyLinePlaceholder":233},[1173,2699,2701,2703,2705],{"class":1175,"line":2700},46,[1173,2702,2247],{"class":1269},[1173,2704,2416],{"class":1185},[1173,2706,2707],{"class":1269},"(): KeyPair {\n",[1173,2709,2711],{"class":1175,"line":2710},47,[1173,2712,2713],{"class":1179}," // Return the newest non-expired key\n",[1173,2715,2717,2719,2722,2724,2726,2729,2731,2734],{"class":1175,"line":2716},48,[1173,2718,1614],{"class":1215},[1173,2720,2721],{"class":1269}," Array.",[1173,2723,1496],{"class":1185},[1173,2725,1520],{"class":1269},[1173,2727,2728],{"class":1238},"this",[1173,2730,2374],{"class":1269},[1173,2732,2733],{"class":1185},"values",[1173,2735,2736],{"class":1269},"())\n",[1173,2738,2740,2743,2746,2748,2750,2752,2755,2757,2759,2762,2765,2767,2770,2773,2775,2778,2781],{"class":1175,"line":2739},49,[1173,2741,2742],{"class":1269}," .",[1173,2744,2745],{"class":1185},"sort",[1173,2747,2356],{"class":1269},[1173,2749,171],{"class":1523},[1173,2751,1327],{"class":1269},[1173,2753,2754],{"class":1523},"b",[1173,2756,2366],{"class":1269},[1173,2758,2369],{"class":1215},[1173,2760,2761],{"class":1269}," b.createdAt.",[1173,2763,2764],{"class":1185},"getTime",[1173,2766,2631],{"class":1269},[1173,2768,2769],{"class":1215},"-",[1173,2771,2772],{"class":1269}," a.createdAt.",[1173,2774,2764],{"class":1185},[1173,2776,2777],{"class":1269},"())[",[1173,2779,2780],{"class":1238},"0",[1173,2782,2783],{"class":1269},"];\n",[1173,2785,2787],{"class":1175,"line":2786},50,[1173,2788,2151],{"class":1269},[1173,2790,2792],{"class":1175,"line":2791},51,[1173,2793,1630],{"class":1269},[20,2795,2796],{},"The rotation process:",[2070,2798,2799,2805,2808,2811,2814,2817],{},[188,2800,2801,2802],{},"Generate a new signing key with a new ",[287,2803,2804],{},"kid",[188,2806,2807],{},"Add it to your secrets manager alongside the current key",[188,2809,2810],{},"Restart your application — new tokens are signed with the new key",[188,2812,2813],{},"Old tokens signed with the old key are still valid because the old key is still in your key set",[188,2815,2816],{},"After your token expiry period (say, 15 minutes), all active tokens have been re-issued with the new key",[188,2818,2819],{},"Remove the old key from your key set after the expiry period",[15,2821,2823],{"id":2822},"automating-rotation-with-aws-secrets-manager","Automating Rotation with AWS Secrets Manager",[20,2825,2826],{},"For applications in AWS, Secrets Manager provides the most complete rotation automation:",[378,2828,2830],{"className":1481,"code":2829,"language":1483,"meta":213,"style":213},"// Application reads from Secrets Manager at startup and refreshes periodically\nimport { SecretsManagerClient, GetSecretValueCommand } from \"@aws-sdk/client-secrets-manager\";\n\nClass SecretManager {\n private client: SecretsManagerClient;\n private cache: Map\u003Cstring, { value: string; fetchedAt: number }> = new Map();\n private ttl = 5 * 60 * 1000; // 5 minute cache\n\n constructor() {\n this.client = new SecretsManagerClient({ region: \"us-east-1\" });\n }\n\n async get(secretId: string): Promise\u003Cstring> {\n const cached = this.cache.get(secretId);\n if (cached && Date.now() - cached.fetchedAt \u003C this.ttl) {\n return cached.value;\n }\n\n const response = await this.client.send(\n new GetSecretValueCommand({ SecretId: secretId })\n );\n\n const value = response.SecretString ?? \"\";\n this.cache.set(secretId, { value, fetchedAt: Date.now() });\n return value;\n }\n}\n",[287,2831,2832,2837,2849,2853,2858,2868,2892,2920,2924,2930,2949,2953,2957,2978,2995,3022,3036,3040,3044,3066,3086,3090,3094,3117,3156,3164,3168],{"__ignoreMap":213},[1173,2833,2834],{"class":1175,"line":1176},[1173,2835,2836],{"class":1179},"// Application reads from Secrets Manager at startup and refreshes periodically\n",[1173,2838,2839,2841,2843,2845,2847],{"class":1175,"line":217},[1173,2840,1490],{"class":1215},[1173,2842,1493],{"class":1269},[1173,2844,1496],{"class":1215},[1173,2846,1499],{"class":1189},[1173,2848,1502],{"class":1269},[1173,2850,2851],{"class":1175,"line":214},[1173,2852,1198],{"emptyLinePlaceholder":233},[1173,2854,2855],{"class":1175,"line":1201},[1173,2856,2857],{"class":1269},"Class SecretManager {\n",[1173,2859,2860,2862,2865],{"class":1175,"line":1207},[1173,2861,2247],{"class":1269},[1173,2863,2864],{"class":1185},"client",[1173,2866,2867],{"class":1269},": SecretsManagerClient;\n",[1173,2869,2870,2872,2875,2877,2879,2882,2884,2886,2888,2890],{"class":1175,"line":1222},[1173,2871,2247],{"class":1269},[1173,2873,2874],{"class":1185},"cache",[1173,2876,2253],{"class":1269},[1173,2878,1541],{"class":1215},[1173,2880,2881],{"class":1269},"string, { value: string; fetchedAt: number }",[1173,2883,2261],{"class":1215},[1173,2885,1558],{"class":1215},[1173,2887,1561],{"class":1215},[1173,2889,2268],{"class":1185},[1173,2891,2271],{"class":1269},[1173,2893,2894,2897,2900,2903,2906,2909,2911,2914,2917],{"class":1175,"line":235},[1173,2895,2896],{"class":1269}," private ttl ",[1173,2898,2899],{"class":1215},"=",[1173,2901,2902],{"class":1238}," 5",[1173,2904,2905],{"class":1215}," *",[1173,2907,2908],{"class":1238}," 60",[1173,2910,2905],{"class":1215},[1173,2912,2913],{"class":1238}," 1000",[1173,2915,2916],{"class":1269},"; ",[1173,2918,2919],{"class":1179},"// 5 minute cache\n",[1173,2921,2922],{"class":1175,"line":1232},[1173,2923,1198],{"emptyLinePlaceholder":233},[1173,2925,2926,2928],{"class":1175,"line":877},[1173,2927,2280],{"class":1185},[1173,2929,2283],{"class":1269},[1173,2931,2932,2934,2937,2939,2941,2943,2945,2947],{"class":1175,"line":1351},[1173,2933,2288],{"class":1238},[1173,2935,2936],{"class":1269},".client ",[1173,2938,2899],{"class":1215},[1173,2940,1561],{"class":1215},[1173,2942,1564],{"class":1185},[1173,2944,1567],{"class":1269},[1173,2946,1570],{"class":1189},[1173,2948,1573],{"class":1269},[1173,2950,2951],{"class":1175,"line":1361},[1173,2952,2151],{"class":1269},[1173,2954,2955],{"class":1175,"line":1368},[1173,2956,1198],{"emptyLinePlaceholder":233},[1173,2958,2959,2962,2964,2967,2970,2972,2974,2976],{"class":1175,"line":1785},[1173,2960,2961],{"class":1269}," async ",[1173,2963,2602],{"class":1185},[1173,2965,2966],{"class":1269},"(secretId: string): ",[1173,2968,2969],{"class":1238},"Promise",[1173,2971,1541],{"class":1215},[1173,2973,1544],{"class":1269},[1173,2975,2261],{"class":1215},[1173,2977,2184],{"class":1269},[1173,2979,2980,2983,2985,2987,2990,2992],{"class":1175,"line":1796},[1173,2981,2982],{"class":1269}," const cached ",[1173,2984,2899],{"class":1215},[1173,2986,2288],{"class":1238},[1173,2988,2989],{"class":1269},".cache.",[1173,2991,2602],{"class":1185},[1173,2993,2994],{"class":1269},"(secretId);\n",[1173,2996,2997,2999,3001,3004,3007,3010,3013,3015,3017,3020],{"class":1175,"line":1804},[1173,2998,2513],{"class":1185},[1173,3000,2516],{"class":1269},[1173,3002,3003],{"class":1523},"cached",[1173,3005,3006],{"class":1269}," && Date.now() - cached.",[1173,3008,3009],{"class":1185},"fetchedAt",[1173,3011,3012],{"class":1269}," \u003C ",[1173,3014,2728],{"class":1185},[1173,3016,177],{"class":1269},[1173,3018,3019],{"class":1185},"ttl",[1173,3021,2539],{"class":1269},[1173,3023,3024,3026,3029,3031,3034],{"class":1175,"line":1817},[1173,3025,1614],{"class":1185},[1173,3027,3028],{"class":1185}," cached",[1173,3030,177],{"class":1269},[1173,3032,3033],{"class":1523},"value",[1173,3035,1502],{"class":1269},[1173,3037,3038],{"class":1175,"line":1825},[1173,3039,2151],{"class":1269},[1173,3041,3042],{"class":1175,"line":2347},[1173,3043,1198],{"emptyLinePlaceholder":233},[1173,3045,3046,3048,3050,3052,3054,3056,3058,3060,3062,3064],{"class":1175,"line":2383},[1173,3047,1552],{"class":1215},[1173,3049,1580],{"class":1185},[1173,3051,1558],{"class":1215},[1173,3053,1585],{"class":1185},[1173,3055,2288],{"class":1185},[1173,3057,177],{"class":1269},[1173,3059,2864],{"class":1185},[1173,3061,177],{"class":1269},[1173,3063,1591],{"class":1185},[1173,3065,1594],{"class":1269},[1173,3067,3068,3070,3072,3075,3078,3080,3083],{"class":1175,"line":2388},[1173,3069,1561],{"class":1185},[1173,3071,1601],{"class":1185},[1173,3073,3074],{"class":1269},"({ ",[1173,3076,3077],{"class":1523},"SecretId",[1173,3079,1527],{"class":1215},[1173,3081,3082],{"class":1185}," secretId",[1173,3084,3085],{"class":1269}," })\n",[1173,3087,3088],{"class":1175,"line":2393},[1173,3089,1609],{"class":1269},[1173,3091,3092],{"class":1175,"line":2402},[1173,3093,1198],{"emptyLinePlaceholder":233},[1173,3095,3096,3098,3101,3103,3105,3107,3110,3113,3115],{"class":1175,"line":2421},[1173,3097,1552],{"class":1215},[1173,3099,3100],{"class":1185}," value",[1173,3102,1558],{"class":1215},[1173,3104,1580],{"class":1185},[1173,3106,177],{"class":1269},[1173,3108,3109],{"class":1185},"SecretString",[1173,3111,3112],{"class":1215}," ??",[1173,3114,1623],{"class":1189},[1173,3116,1502],{"class":1269},[1173,3118,3119,3121,3123,3125,3127,3129,3131,3134,3137,3139,3141,3143,3145,3148,3150,3153],{"class":1175,"line":2435},[1173,3120,2288],{"class":1185},[1173,3122,177],{"class":1269},[1173,3124,2874],{"class":1185},[1173,3126,177],{"class":1269},[1173,3128,2377],{"class":1185},[1173,3130,1520],{"class":1269},[1173,3132,3133],{"class":1523},"secretId",[1173,3135,3136],{"class":1269},", { ",[1173,3138,3033],{"class":1523},[1173,3140,1327],{"class":1269},[1173,3142,3009],{"class":1523},[1173,3144,1281],{"class":1269},[1173,3146,3147],{"class":1523},"Date",[1173,3149,177],{"class":1269},[1173,3151,3152],{"class":1523},"now",[1173,3154,3155],{"class":1269},"() });\n",[1173,3157,3158,3160,3162],{"class":1175,"line":2446},[1173,3159,1614],{"class":1185},[1173,3161,3100],{"class":1185},[1173,3163,1502],{"class":1269},[1173,3165,3166],{"class":1175,"line":2452},[1173,3167,2151],{"class":1269},[1173,3169,3170],{"class":1175,"line":2463},[1173,3171,1630],{"class":1269},[20,3173,3174],{},"Caching secrets with a short TTL means your application periodically refreshes from Secrets Manager. When a rotation occurs, your application picks up the new secret within five minutes without a restart.",[15,3176,3178],{"id":3177},"rotation-for-self-hosted-deployments","Rotation for Self-Hosted Deployments",[20,3180,3181],{},"Without a managed service like AWS Secrets Manager, implement rotation as a scheduled job:",[378,3183,3185],{"className":1481,"code":3184,"language":1483,"meta":213,"style":213},"// Rotation script run weekly via cron\nasync function rotateApiDatabasePassword() {\n const newPassword = generateStrongPassword();\n\n // Phase 1: Add new password to database user (keep old password)\n await db.execute(\n `ALTER USER api_app PASSWORD '${newPassword}' VALID UNTIL 'now' + interval '7 days'`\n );\n\n // Phase 2: Update secrets in your secrets store\n await doppler.updateSecret(\"DATABASE_PASSWORD\", newPassword);\n\n // Phase 3: Notify that restart is needed (or trigger rolling restart)\n await notifySlack(\"#ops\", \"Database password rotated. Rolling restart initiated.\");\n await triggerRollingRestart(\"api\");\n\n // Phase 4 (after restart completes): Remove old password alternative\n // ... Implementation depends on your database\n}\n",[287,3186,3187,3192,3205,3219,3223,3228,3240,3251,3255,3259,3264,3282,3286,3291,3310,3324,3328,3333,3338],{"__ignoreMap":213},[1173,3188,3189],{"class":1175,"line":1176},[1173,3190,3191],{"class":1179},"// Rotation script run weekly via cron\n",[1173,3193,3194,3197,3200,3203],{"class":1175,"line":217},[1173,3195,3196],{"class":1215},"async",[1173,3198,3199],{"class":1215}," function",[1173,3201,3202],{"class":1185}," rotateApiDatabasePassword",[1173,3204,2283],{"class":1269},[1173,3206,3207,3209,3212,3214,3217],{"class":1175,"line":214},[1173,3208,1552],{"class":1215},[1173,3210,3211],{"class":1238}," newPassword",[1173,3213,1558],{"class":1215},[1173,3215,3216],{"class":1185}," generateStrongPassword",[1173,3218,2271],{"class":1269},[1173,3220,3221],{"class":1175,"line":1201},[1173,3222,1198],{"emptyLinePlaceholder":233},[1173,3224,3225],{"class":1175,"line":1207},[1173,3226,3227],{"class":1179}," // Phase 1: Add new password to database user (keep old password)\n",[1173,3229,3230,3232,3235,3238],{"class":1175,"line":1222},[1173,3231,1585],{"class":1215},[1173,3233,3234],{"class":1269}," db.",[1173,3236,3237],{"class":1185},"execute",[1173,3239,1594],{"class":1269},[1173,3241,3242,3245,3248],{"class":1175,"line":235},[1173,3243,3244],{"class":1189}," `ALTER USER api_app PASSWORD '${",[1173,3246,3247],{"class":1269},"newPassword",[1173,3249,3250],{"class":1189},"}' VALID UNTIL 'now' + interval '7 days'`\n",[1173,3252,3253],{"class":1175,"line":1232},[1173,3254,1609],{"class":1269},[1173,3256,3257],{"class":1175,"line":877},[1173,3258,1198],{"emptyLinePlaceholder":233},[1173,3260,3261],{"class":1175,"line":1351},[1173,3262,3263],{"class":1179}," // Phase 2: Update secrets in your secrets store\n",[1173,3265,3266,3268,3271,3274,3276,3279],{"class":1175,"line":1361},[1173,3267,1585],{"class":1215},[1173,3269,3270],{"class":1269}," doppler.",[1173,3272,3273],{"class":1185},"updateSecret",[1173,3275,1520],{"class":1269},[1173,3277,3278],{"class":1189},"\"DATABASE_PASSWORD\"",[1173,3280,3281],{"class":1269},", newPassword);\n",[1173,3283,3284],{"class":1175,"line":1368},[1173,3285,1198],{"emptyLinePlaceholder":233},[1173,3287,3288],{"class":1175,"line":1785},[1173,3289,3290],{"class":1179}," // Phase 3: Notify that restart is needed (or trigger rolling restart)\n",[1173,3292,3293,3295,3298,3300,3303,3305,3308],{"class":1175,"line":1796},[1173,3294,1585],{"class":1215},[1173,3296,3297],{"class":1185}," notifySlack",[1173,3299,1520],{"class":1269},[1173,3301,3302],{"class":1189},"\"#ops\"",[1173,3304,1327],{"class":1269},[1173,3306,3307],{"class":1189},"\"Database password rotated. Rolling restart initiated.\"",[1173,3309,1661],{"class":1269},[1173,3311,3312,3314,3317,3319,3322],{"class":1175,"line":1804},[1173,3313,1585],{"class":1215},[1173,3315,3316],{"class":1185}," triggerRollingRestart",[1173,3318,1520],{"class":1269},[1173,3320,3321],{"class":1189},"\"api\"",[1173,3323,1661],{"class":1269},[1173,3325,3326],{"class":1175,"line":1817},[1173,3327,1198],{"emptyLinePlaceholder":233},[1173,3329,3330],{"class":1175,"line":1825},[1173,3331,3332],{"class":1179}," // Phase 4 (after restart completes): Remove old password alternative\n",[1173,3334,3335],{"class":1175,"line":2347},[1173,3336,3337],{"class":1179}," // ... Implementation depends on your database\n",[1173,3339,3340],{"class":1175,"line":2383},[1173,3341,1630],{"class":1269},[20,3343,3344],{},"The gap in self-hosted rotation is that phase 4 (removing old credentials after the application has transitioned) requires careful timing. Managed rotation services handle this automatically.",[15,3346,3348],{"id":3347},"the-rotation-audit-trail","The Rotation Audit Trail",[20,3350,3351],{},"Every rotation should be logged: what was rotated, when, by what process (automated or manual), and who triggered it if manual. This audit trail is essential for:",[185,3353,3354,3357,3360],{},[188,3355,3356],{},"Incident response: if a credential was compromised, knowing exactly when it was last rotated tells you the exposure window",[188,3358,3359],{},"Compliance: many frameworks require evidence of credential rotation",[188,3361,3362],{},"Debugging: if an authentication failure appears after a rotation, knowing the rotation timestamp helps narrow the cause",[20,3364,3365],{},"Store rotation logs in your centralized logging system, separate from the systems the credentials access. Logs stored on the system protected by the credentials can be tampered with if those credentials are compromised.",[27,3367],{},[20,3369,3370,3371,177],{},"If you want help designing an automated rotation strategy for your application's credentials or need to audit your current rotation practices, book a session at ",[171,3372,173],{"href":173,"rel":3373},[175],[27,3375],{},[15,3377,183],{"id":182},[185,3379,3380,3386,3392,3398],{},[188,3381,3382],{},[171,3383,3385],{"href":3384},"/blog/api-security-best-practices","API Security Best Practices: Protecting Your Endpoints in Production",[188,3387,3388],{},[171,3389,3391],{"href":3390},"/blog/authentication-security-guide","Authentication Security: What to Get Right Before Your First User Logs In",[188,3393,3394],{},[171,3395,3397],{"href":3396},"/blog/csrf-protection-guide","CSRF Protection: Understanding Cross-Site Request Forgery and Stopping It",[188,3399,3400],{},[171,3401,3403],{"href":3402},"/blog/content-security-policy-guide","Content Security Policy: Stopping XSS at the Browser Level",[1971,3405,3406],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":213,"searchDepth":214,"depth":214,"links":3408},[3409,3410,3411,3412,3413,3414,3415],{"id":2016,"depth":217,"text":2017},{"id":2056,"depth":217,"text":2057},{"id":2161,"depth":217,"text":2162},{"id":2822,"depth":217,"text":2823},{"id":3177,"depth":217,"text":3178},{"id":3347,"depth":217,"text":3348},{"id":182,"depth":217,"text":183},"Why credential rotation matters, what happens when you do not rotate, and how to implement automated secrets rotation for database passwords, API keys, and JWT secrets.",[3418,3419],"secrets rotation","credential management",{},"/blog/secrets-rotation-guide",{"title":1998,"description":3416},"blog/secrets-rotation-guide",[3425,555,1994,3426],"Secrets Rotation","Automation","Pc23J53HXn80_5zkxglTTSLq2rKplc0_mS-SkmjYY94",{"id":3429,"title":3430,"author":3431,"body":3432,"category":555,"date":224,"description":4160,"extension":226,"featured":227,"image":228,"keywords":4161,"meta":4164,"navigation":233,"path":4165,"readTime":235,"seo":4166,"stem":4167,"tags":4168,"__hash__":4172},"blog/blog/security-headers-web-apps.md","Security Headers for Web Applications: The Complete Configuration Guide",{"name":9,"bio":10},{"type":12,"value":3433,"toc":4147},[3434,3437,3440,3443,3447,3450,3456,3459,3465,3475,3484,3490,3500,3510,3516,3522,3525,3531,3534,3538,3541,3547,3561,3568,3572,3579,3585,3595,3601,3605,3608,3614,3617,3621,3628,3634,3640,3643,3647,3650,3656,3670,3677,3681,3684,3690,3700,3710,3717,3721,3728,3953,3959,4030,4034,4037,4074,4080,4084,4087,4107,4110,4112,4118,4120,4122,4144],[1125,3435,3430],{"id":3436},"security-headers-for-web-applications-the-complete-configuration-guide",[20,3438,3439],{},"HTTP security headers are browser instructions about how your application should be handled. They provide defense against cross-site scripting, clickjacking, MIME sniffing, and a range of other attacks — without a single line of application logic change. They are added to HTTP responses, they are free, and most applications are not configured with them correctly.",[20,3441,3442],{},"I am going to go through every meaningful security header, what it does, and exactly how to configure it. At the end, you will have a complete configuration you can deploy.",[15,3444,3446],{"id":3445},"content-security-policy","Content-Security-Policy",[20,3448,3449],{},"CSP is the most powerful and most complex security header. It tells the browser which resources (scripts, styles, images, fonts, API calls) are allowed to load on your page. A well-configured CSP prevents XSS from executing even if an attacker successfully injects script content.",[378,3451,3454],{"className":3452,"code":3453,"language":383},[381],"Content-Security-Policy:\n default-src 'self';\n script-src 'self' https://cdn.yourdomain.com;\n style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;\n font-src 'self' https://fonts.gstatic.com;\n img-src 'self' data: https:;\n connect-src 'self' https://api.yourdomain.com;\n media-src 'none';\n object-src 'none';\n frame-src 'none';\n frame-ancestors 'none';\n base-uri 'self';\n form-action 'self';\n upgrade-insecure-requests;\n",[287,3455,3453],{"__ignoreMap":213},[20,3457,3458],{},"Let me explain each directive:",[20,3460,3461,3464],{},[287,3462,3463],{},"default-src 'self'"," — the fallback for any resource type not explicitly listed. Only allow resources from your own origin.",[20,3466,3467,3470,3471,3474],{},[287,3468,3469],{},"script-src 'self' https://cdn.yourdomain.com"," — only execute scripts from your origin and your CDN. This blocks inline scripts and scripts from unknown domains. No ",[287,3472,3473],{},"'unsafe-inline'"," unless absolutely necessary.",[20,3476,3477,3480,3481,3483],{},[287,3478,3479],{},"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com"," — inline styles are common in modern frameworks (styled-components, emotion, inline Tailwind), so ",[287,3482,3473],{}," for styles is often necessary. This does not create an XSS vulnerability because CSS cannot execute JavaScript (with rare exceptions that are covered by other directives).",[20,3485,3486,3489],{},[287,3487,3488],{},"object-src 'none'"," — disable Flash, Java applets, and other plugins entirely. There is no legitimate reason to enable these in 2026.",[20,3491,3492,3495,3496,3499],{},[287,3493,3494],{},"frame-ancestors 'none'"," — equivalent to ",[287,3497,3498],{},"X-Frame-Options: DENY",". Your page cannot be embedded in any iframe. Prevents clickjacking.",[20,3501,3502,3505,3506,3509],{},[287,3503,3504],{},"base-uri 'self'"," — restricts ",[287,3507,3508],{},"\u003Cbase>"," tag targets. Prevents base tag injection attacks where attackers change the base URL for relative links.",[20,3511,3512,3515],{},[287,3513,3514],{},"form-action 'self'"," — forms can only submit to your own origin. Prevents form hijacking attacks.",[20,3517,3518,3521],{},[287,3519,3520],{},"upgrade-insecure-requests"," — automatically upgrades HTTP URLs on your page to HTTPS. Fixes mixed content issues.",[20,3523,3524],{},"Start with CSP in report-only mode:",[378,3526,3529],{"className":3527,"code":3528,"language":383},[381],"Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-violations\n",[287,3530,3528],{"__ignoreMap":213},[20,3532,3533],{},"This logs violations without blocking anything. Review the report logs, adjust your policy to allow legitimate resources, then switch to enforcement mode.",[15,3535,3537],{"id":3536},"strict-transport-security-hsts","Strict-Transport-Security (HSTS)",[20,3539,3540],{},"Forces HTTPS for all connections to your domain. Once a browser sees this header, it remembers to use HTTPS for your domain for the specified duration without any HTTP round trip.",[378,3542,3545],{"className":3543,"code":3544,"language":383},[381],"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",[287,3546,3544],{"__ignoreMap":213},[20,3548,3549,3552,3553,3556,3557,3560],{},[287,3550,3551],{},"max-age=31536000"," — remember for one year.\n",[287,3554,3555],{},"includeSubDomains"," — apply to all subdomains.\n",[287,3558,3559],{},"preload"," — signal readiness for browser preload lists. Submit at hstspreload.org to get included.",[20,3562,3563,3564,3567],{},"Start with a short ",[287,3565,3566],{},"max-age"," (300 seconds), verify your entire site works over HTTPS, then increase to one year. HSTS with broken HTTPS means users cannot reach your site.",[15,3569,3571],{"id":3570},"x-frame-options","X-Frame-Options",[20,3573,3574,3575,3578],{},"Older equivalent to CSP's ",[287,3576,3577],{},"frame-ancestors",". Controls iframe embedding:",[378,3580,3583],{"className":3581,"code":3582,"language":383},[381],"X-Frame-Options: DENY\n",[287,3584,3582],{"__ignoreMap":213},[20,3586,3587,3590,3591,3594],{},[287,3588,3589],{},"DENY"," — your page cannot be embedded anywhere.\n",[287,3592,3593],{},"SAMEORIGIN"," — can be embedded by your own domain only.",[20,3596,3597,3598,3600],{},"Keep this header even if you have ",[287,3599,3577],{}," in your CSP for compatibility with older browsers.",[15,3602,3604],{"id":3603},"x-content-type-options","X-Content-Type-Options",[20,3606,3607],{},"Prevents browsers from \"sniffing\" content types. Without this header, a browser might execute a JavaScript file served with a misleading content type:",[378,3609,3612],{"className":3610,"code":3611,"language":383},[381],"X-Content-Type-Options: nosniff\n",[287,3613,3611],{"__ignoreMap":213},[20,3615,3616],{},"Always include this. It has no side effects on correctly configured servers and prevents a class of file upload and content injection attacks.",[15,3618,3620],{"id":3619},"referrer-policy","Referrer-Policy",[20,3622,3623,3624,3627],{},"Controls how much information is included in the ",[287,3625,3626],{},"Referer"," header sent with outgoing requests. Without this header, clicking a link from your page to an external site sends your full URL to that external site, including query parameters that might contain user IDs, session tokens, or private data.",[378,3629,3632],{"className":3630,"code":3631,"language":383},[381],"Referrer-Policy: strict-origin-when-cross-origin\n",[287,3633,3631],{"__ignoreMap":213},[20,3635,3636,3639],{},[287,3637,3638],{},"strict-origin-when-cross-origin"," — for same-origin requests, send the full URL. For cross-origin requests, send only the origin (no path or query string). For cross-origin requests to a less-secure scheme (HTTPS to HTTP), send nothing.",[20,3641,3642],{},"This is the recommended value for most applications. It preserves referrer analytics for same-origin navigation while protecting sensitive URL parameters from leaking to third parties.",[15,3644,3646],{"id":3645},"permissions-policy","Permissions-Policy",[20,3648,3649],{},"Formerly Feature-Policy. Restricts access to browser APIs and features. Disable everything your application does not use:",[378,3651,3654],{"className":3652,"code":3653,"language":383},[381],"Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()\n",[287,3655,3653],{"__ignoreMap":213},[20,3657,3658,3659,3662,3663,3666,3667,177],{},"Each empty ",[287,3660,3661],{},"()"," disables the feature entirely. Enabling features for your origin: ",[287,3664,3665],{},"camera=(self)",". Enabling for a specific external origin: ",[287,3668,3669],{},"camera=(\"https://video.yourdomain.com\")",[20,3671,3672,3673,3676],{},"Note: ",[287,3674,3675],{},"interest-cohort=()"," disables FLoC (Google's Federated Learning of Cohorts), which uses your site to profile users for ad targeting. This opt-out is good practice regardless of your security posture.",[15,3678,3680],{"id":3679},"cross-origin-headers","Cross-Origin Headers",[20,3682,3683],{},"Three newer headers that affect how your resources are shared across origins. These matter for isolation and Spectre mitigation:",[378,3685,3688],{"className":3686,"code":3687,"language":383},[381],"Cross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\nCross-Origin-Resource-Policy: same-origin\n",[287,3689,3687],{"__ignoreMap":213},[20,3691,3692,3695,3696,3699],{},[287,3693,3694],{},"COOP: same-origin"," — prevents other origins from accessing your window via ",[287,3697,3698],{},"window.opener",". Provides isolation from cross-origin popups.",[20,3701,3702,3705,3706,3709],{},[287,3703,3704],{},"COEP: require-corp"," — requires all resources loaded by your page to opt in to cross-origin sharing. Combined with COOP, enables access to ",[287,3707,3708],{},"SharedArrayBuffer"," (needed for high-performance web applications using WebAssembly) and provides Spectre mitigations.",[20,3711,3712,3713,3716],{},"These headers can break third-party embeds if those resources do not send appropriate ",[287,3714,3715],{},"Cross-Origin-Resource-Policy"," headers. Implement with testing.",[15,3718,3720],{"id":3719},"implementation-in-nodejs-with-helmet","Implementation in Node.js with Helmet",[20,3722,3723,3724,3727],{},"The ",[287,3725,3726],{},"helmet"," middleware for Express sets all these headers with sensible defaults:",[378,3729,3731],{"className":1481,"code":3730,"language":1483,"meta":213,"style":213},"import helmet from \"helmet\";\n\nApp.use(\n helmet({\n contentSecurityPolicy: {\n directives: {\n defaultSrc: [\"'self'\"],\n scriptSrc: [\"'self'\", \"https://cdn.yourdomain.com\"],\n styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n imgSrc: [\"'self'\", \"data:\", \"https:\"],\n connectSrc: [\"'self'\", \"https://api.yourdomain.com\"],\n frameSrc: [\"'none'\"],\n objectSrc: [\"'none'\"],\n },\n },\n hsts: {\n maxAge: 31536000,\n includeSubDomains: true,\n preload: true,\n },\n referrerPolicy: { policy: \"strict-origin-when-cross-origin\" },\n frameguard: { action: \"deny\" },\n noSniff: true,\n })\n);\n",[287,3732,3733,3747,3751,3761,3769,3774,3779,3790,3804,3818,3837,3851,3861,3870,3875,3879,3884,3894,3903,3912,3916,3926,3936,3945,3949],{"__ignoreMap":213},[1173,3734,3735,3737,3740,3742,3745],{"class":1175,"line":1176},[1173,3736,1490],{"class":1215},[1173,3738,3739],{"class":1269}," helmet ",[1173,3741,1496],{"class":1215},[1173,3743,3744],{"class":1189}," \"helmet\"",[1173,3746,1502],{"class":1269},[1173,3748,3749],{"class":1175,"line":217},[1173,3750,1198],{"emptyLinePlaceholder":233},[1173,3752,3753,3756,3759],{"class":1175,"line":214},[1173,3754,3755],{"class":1269},"App.",[1173,3757,3758],{"class":1185},"use",[1173,3760,1594],{"class":1269},[1173,3762,3763,3766],{"class":1175,"line":1201},[1173,3764,3765],{"class":1185}," helmet",[1173,3767,3768],{"class":1269},"({\n",[1173,3770,3771],{"class":1175,"line":1207},[1173,3772,3773],{"class":1269}," contentSecurityPolicy: {\n",[1173,3775,3776],{"class":1175,"line":1222},[1173,3777,3778],{"class":1269}," directives: {\n",[1173,3780,3781,3784,3787],{"class":1175,"line":235},[1173,3782,3783],{"class":1269}," defaultSrc: [",[1173,3785,3786],{"class":1189},"\"'self'\"",[1173,3788,3789],{"class":1269},"],\n",[1173,3791,3792,3795,3797,3799,3802],{"class":1175,"line":1232},[1173,3793,3794],{"class":1269}," scriptSrc: [",[1173,3796,3786],{"class":1189},[1173,3798,1327],{"class":1269},[1173,3800,3801],{"class":1189},"\"https://cdn.yourdomain.com\"",[1173,3803,3789],{"class":1269},[1173,3805,3806,3809,3811,3813,3816],{"class":1175,"line":877},[1173,3807,3808],{"class":1269}," styleSrc: [",[1173,3810,3786],{"class":1189},[1173,3812,1327],{"class":1269},[1173,3814,3815],{"class":1189},"\"'unsafe-inline'\"",[1173,3817,3789],{"class":1269},[1173,3819,3820,3823,3825,3827,3830,3832,3835],{"class":1175,"line":1351},[1173,3821,3822],{"class":1269}," imgSrc: [",[1173,3824,3786],{"class":1189},[1173,3826,1327],{"class":1269},[1173,3828,3829],{"class":1189},"\"data:\"",[1173,3831,1327],{"class":1269},[1173,3833,3834],{"class":1189},"\"https:\"",[1173,3836,3789],{"class":1269},[1173,3838,3839,3842,3844,3846,3849],{"class":1175,"line":1361},[1173,3840,3841],{"class":1269}," connectSrc: [",[1173,3843,3786],{"class":1189},[1173,3845,1327],{"class":1269},[1173,3847,3848],{"class":1189},"\"https://api.yourdomain.com\"",[1173,3850,3789],{"class":1269},[1173,3852,3853,3856,3859],{"class":1175,"line":1368},[1173,3854,3855],{"class":1269}," frameSrc: [",[1173,3857,3858],{"class":1189},"\"'none'\"",[1173,3860,3789],{"class":1269},[1173,3862,3863,3866,3868],{"class":1175,"line":1785},[1173,3864,3865],{"class":1269}," objectSrc: [",[1173,3867,3858],{"class":1189},[1173,3869,3789],{"class":1269},[1173,3871,3872],{"class":1175,"line":1796},[1173,3873,3874],{"class":1269}," },\n",[1173,3876,3877],{"class":1175,"line":1804},[1173,3878,3874],{"class":1269},[1173,3880,3881],{"class":1175,"line":1817},[1173,3882,3883],{"class":1269}," hsts: {\n",[1173,3885,3886,3889,3892],{"class":1175,"line":1825},[1173,3887,3888],{"class":1269}," maxAge: ",[1173,3890,3891],{"class":1238},"31536000",[1173,3893,2116],{"class":1269},[1173,3895,3896,3899,3901],{"class":1175,"line":2347},[1173,3897,3898],{"class":1269}," includeSubDomains: ",[1173,3900,2505],{"class":1238},[1173,3902,2116],{"class":1269},[1173,3904,3905,3908,3910],{"class":1175,"line":2383},[1173,3906,3907],{"class":1269}," preload: ",[1173,3909,2505],{"class":1238},[1173,3911,2116],{"class":1269},[1173,3913,3914],{"class":1175,"line":2388},[1173,3915,3874],{"class":1269},[1173,3917,3918,3921,3924],{"class":1175,"line":2393},[1173,3919,3920],{"class":1269}," referrerPolicy: { policy: ",[1173,3922,3923],{"class":1189},"\"strict-origin-when-cross-origin\"",[1173,3925,3874],{"class":1269},[1173,3927,3928,3931,3934],{"class":1175,"line":2402},[1173,3929,3930],{"class":1269}," frameguard: { action: ",[1173,3932,3933],{"class":1189},"\"deny\"",[1173,3935,3874],{"class":1269},[1173,3937,3938,3941,3943],{"class":1175,"line":2421},[1173,3939,3940],{"class":1269}," noSniff: ",[1173,3942,2505],{"class":1238},[1173,3944,2116],{"class":1269},[1173,3946,3947],{"class":1175,"line":2435},[1173,3948,3085],{"class":1269},[1173,3950,3951],{"class":1175,"line":2446},[1173,3952,1661],{"class":1269},[20,3954,3955,3956,3958],{},"Helmet covers the major headers. Add ",[287,3957,3646],{}," manually since Helmet does not include it by default:",[378,3960,3962],{"className":1481,"code":3961,"language":1483,"meta":213,"style":213},"app.use((req, res, next) => {\n res.setHeader(\n \"Permissions-Policy\",\n \"camera=(), microphone=(), geolocation=(), interest-cohort=()\"\n );\n next();\n});\n",[287,3963,3964,3992,4002,4009,4014,4018,4025],{"__ignoreMap":213},[1173,3965,3966,3969,3971,3973,3976,3978,3981,3983,3986,3988,3990],{"class":1175,"line":1176},[1173,3967,3968],{"class":1269},"app.",[1173,3970,3758],{"class":1185},[1173,3972,2356],{"class":1269},[1173,3974,3975],{"class":1523},"req",[1173,3977,1327],{"class":1269},[1173,3979,3980],{"class":1523},"res",[1173,3982,1327],{"class":1269},[1173,3984,3985],{"class":1523},"next",[1173,3987,2366],{"class":1269},[1173,3989,2369],{"class":1215},[1173,3991,2184],{"class":1269},[1173,3993,3994,3997,4000],{"class":1175,"line":217},[1173,3995,3996],{"class":1269}," res.",[1173,3998,3999],{"class":1185},"setHeader",[1173,4001,1594],{"class":1269},[1173,4003,4004,4007],{"class":1175,"line":214},[1173,4005,4006],{"class":1189}," \"Permissions-Policy\"",[1173,4008,2116],{"class":1269},[1173,4010,4011],{"class":1175,"line":1201},[1173,4012,4013],{"class":1189}," \"camera=(), microphone=(), geolocation=(), interest-cohort=()\"\n",[1173,4015,4016],{"class":1175,"line":1207},[1173,4017,1609],{"class":1269},[1173,4019,4020,4023],{"class":1175,"line":1222},[1173,4021,4022],{"class":1185}," next",[1173,4024,2271],{"class":1269},[1173,4026,4027],{"class":1175,"line":235},[1173,4028,4029],{"class":1269},"});\n",[15,4031,4033],{"id":4032},"implementation-in-nginx","Implementation in Nginx",[20,4035,4036],{},"Set headers at the server block level for all responses:",[378,4038,4042],{"className":4039,"code":4040,"language":4041,"meta":213,"style":213},"language-nginx shiki shiki-themes github-dark","add_header Content-Security-Policy \"default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none';\" always;\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\nadd_header X-Frame-Options \"DENY\" always;\nadd_header X-Content-Type-Options \"nosniff\" always;\nadd_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\nadd_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n","nginx",[287,4043,4044,4049,4054,4059,4064,4069],{"__ignoreMap":213},[1173,4045,4046],{"class":1175,"line":1176},[1173,4047,4048],{},"add_header Content-Security-Policy \"default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none';\" always;\n",[1173,4050,4051],{"class":1175,"line":217},[1173,4052,4053],{},"add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n",[1173,4055,4056],{"class":1175,"line":214},[1173,4057,4058],{},"add_header X-Frame-Options \"DENY\" always;\n",[1173,4060,4061],{"class":1175,"line":1201},[1173,4062,4063],{},"add_header X-Content-Type-Options \"nosniff\" always;\n",[1173,4065,4066],{"class":1175,"line":1207},[1173,4067,4068],{},"add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n",[1173,4070,4071],{"class":1175,"line":1222},[1173,4072,4073],{},"add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n",[20,4075,3723,4076,4079],{},[287,4077,4078],{},"always"," directive ensures headers are sent even for error responses.",[15,4081,4083],{"id":4082},"verification","Verification",[20,4085,4086],{},"Test your security headers using:",[185,4088,4089,4095,4101],{},[188,4090,4091,4094],{},[36,4092,4093],{},"securityheaders.com"," — grades your header configuration and identifies missing or misconfigured headers",[188,4096,4097,4100],{},[36,4098,4099],{},"Mozilla Observatory"," (observatory.mozilla.org) — comprehensive analysis including TLS configuration",[188,4102,4103,4106],{},[36,4104,4105],{},"SSL Labs"," (ssllabs.com/ssltest) — TLS-specific analysis",[20,4108,4109],{},"Aim for A or A+ on Security Headers and A on Mozilla Observatory. Run these tests after initial deployment and after any significant configuration change.",[27,4111],{},[20,4113,4114,4115,177],{},"If you want help configuring security headers for your web application or reviewing your current security header configuration, book a session at ",[171,4116,173],{"href":173,"rel":4117},[175],[27,4119],{},[15,4121,183],{"id":182},[185,4123,4124,4130,4136,4140],{},[188,4125,4126],{},[171,4127,4129],{"href":4128},"/blog/security-testing-web-apps","Security Testing for Web Applications: What to Test and How",[188,4131,4132],{},[171,4133,4135],{"href":4134},"/blog/input-validation-guide","Input Validation: The First Line of Defense Against Every Attack",[188,4137,4138],{},[171,4139,3385],{"href":3384},[188,4141,4142],{},[171,4143,3391],{"href":3390},[1971,4145,4146],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":214,"depth":214,"links":4148},[4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4159],{"id":3445,"depth":217,"text":3446},{"id":3536,"depth":217,"text":3537},{"id":3570,"depth":217,"text":3571},{"id":3603,"depth":217,"text":3604},{"id":3619,"depth":217,"text":3620},{"id":3645,"depth":217,"text":3646},{"id":3679,"depth":217,"text":3680},{"id":3719,"depth":217,"text":3720},{"id":4032,"depth":217,"text":4033},{"id":4082,"depth":217,"text":4083},{"id":182,"depth":217,"text":183},"Configure HTTP security headers correctly — CSP, HSTS, X-Frame-Options, Permissions-Policy, and every header that protects your web application from common attacks.",[4162,4163],"security headers","HTTP security headers",{},"/blog/security-headers-web-apps",{"title":3430,"description":4160},"blog/security-headers-web-apps",[4169,4170,555,4171],"Security Headers","HTTP Security","Web Application","ehzhVPp1uNK4zQ1mg3TcpcyypNj8_OMwxCFurifds6A",{"id":4174,"title":4129,"author":4175,"body":4176,"category":555,"date":224,"description":4867,"extension":226,"featured":227,"image":228,"keywords":4868,"meta":4871,"navigation":233,"path":4128,"readTime":235,"seo":4872,"stem":4873,"tags":4874,"__hash__":4878},"blog/blog/security-testing-web-apps.md",{"name":9,"bio":10},{"type":12,"value":4177,"toc":4859},[4178,4181,4184,4187,4191,4194,4200,4306,4309,4315,4333,4408,4429,4435,4516,4520,4523,4529,4532,4592,4595,4598,4604,4678,4684,4688,4691,4697,4700,4706,4712,4748,4751,4757,4761,4764,4770,4776,4782,4788,4794,4800,4804,4807,4821,4824,4826,4832,4834,4836,4856],[1125,4179,4129],{"id":4180},"security-testing-for-web-applications-what-to-test-and-how",[20,4182,4183],{},"Security testing exists on a spectrum from fully automated to deeply manual, and from broad surface coverage to narrow targeted analysis. Most development teams need a combination of approaches to achieve meaningful coverage without security testing becoming a full-time job unto itself.",[20,4185,4186],{},"I am going to walk through the practical security testing approaches I use, the specific tools involved, and how to integrate them into a development workflow that does not slow your team down.",[15,4188,4190],{"id":4189},"static-application-security-testing-sast","Static Application Security Testing (SAST)",[20,4192,4193],{},"SAST analyzes your source code for security vulnerabilities without executing it. It finds issues like SQL injection patterns, hardcoded secrets, insecure cryptographic functions, and use of known-vulnerable APIs — at code review time, before the code ships.",[20,4195,4196,4199],{},[36,4197,4198],{},"Semgrep"," is my preferred SAST tool for web applications. It uses pattern-based rules with a community library of security rules for JavaScript, TypeScript, Python, and most other languages. It integrates with CI and runs on every PR:",[378,4201,4203],{"className":1251,"code":4202,"language":1253,"meta":213,"style":213},"- name: Run Semgrep\n uses: semgrep/semgrep-action@v1\n with:\n config: >-\n p/javascript\n p/typescript\n p/nodejs\n p/owasp-top-ten\n generateSarif: \"1\"\n\n- name: Upload SARIF file\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: semgrep.sarif\n",[287,4204,4205,4216,4225,4231,4241,4246,4251,4256,4261,4266,4270,4281,4290,4296],{"__ignoreMap":213},[1173,4206,4207,4209,4211,4213],{"class":1175,"line":1176},[1173,4208,1390],{"class":1269},[1173,4210,1393],{"class":1265},[1173,4212,1281],{"class":1269},[1173,4214,4215],{"class":1189},"Run Semgrep\n",[1173,4217,4218,4220,4222],{"class":1175,"line":217},[1173,4219,1403],{"class":1265},[1173,4221,1281],{"class":1269},[1173,4223,4224],{"class":1189},"semgrep/semgrep-action@v1\n",[1173,4226,4227,4229],{"class":1175,"line":214},[1173,4228,1413],{"class":1265},[1173,4230,1270],{"class":1269},[1173,4232,4233,4236,4238],{"class":1175,"line":1201},[1173,4234,4235],{"class":1265}," config",[1173,4237,1281],{"class":1269},[1173,4239,4240],{"class":1215},">-\n",[1173,4242,4243],{"class":1175,"line":1207},[1173,4244,4245],{"class":1189}," p/javascript\n",[1173,4247,4248],{"class":1175,"line":1222},[1173,4249,4250],{"class":1189}," p/typescript\n",[1173,4252,4253],{"class":1175,"line":235},[1173,4254,4255],{"class":1189}," p/nodejs\n",[1173,4257,4258],{"class":1175,"line":1232},[1173,4259,4260],{"class":1189}," p/owasp-top-ten\n",[1173,4262,4263],{"class":1175,"line":877},[1173,4264,4265],{"class":1189}," generateSarif: \"1\"\n",[1173,4267,4268],{"class":1175,"line":1351},[1173,4269,1198],{"emptyLinePlaceholder":233},[1173,4271,4272,4274,4276,4278],{"class":1175,"line":1361},[1173,4273,1390],{"class":1269},[1173,4275,1393],{"class":1265},[1173,4277,1281],{"class":1269},[1173,4279,4280],{"class":1189},"Upload SARIF file\n",[1173,4282,4283,4285,4287],{"class":1175,"line":1368},[1173,4284,1403],{"class":1265},[1173,4286,1281],{"class":1269},[1173,4288,4289],{"class":1189},"github/codeql-action/upload-sarif@v3\n",[1173,4291,4292,4294],{"class":1175,"line":1785},[1173,4293,1413],{"class":1265},[1173,4295,1270],{"class":1269},[1173,4297,4298,4301,4303],{"class":1175,"line":1796},[1173,4299,4300],{"class":1265}," sarif_file",[1173,4302,1281],{"class":1269},[1173,4304,4305],{"class":1189},"semgrep.sarif\n",[20,4307,4308],{},"Uploading SARIF results to GitHub displays findings inline in the code review interface. Reviewers see security warnings in the same place they see test failures.",[20,4310,4311,4314],{},[36,4312,4313],{},"ESLint security plugins"," catch common security mistakes in JavaScript/TypeScript at the linter level — the fastest feedback loop:",[378,4316,4318],{"className":1167,"code":4317,"language":1169,"meta":213,"style":213},"npm install eslint-plugin-security eslint-plugin-no-unsanitized\n",[287,4319,4320],{"__ignoreMap":213},[1173,4321,4322,4325,4327,4330],{"class":1175,"line":1176},[1173,4323,4324],{"class":1185},"npm",[1173,4326,1190],{"class":1189},[1173,4328,4329],{"class":1189}," eslint-plugin-security",[1173,4331,4332],{"class":1189}," eslint-plugin-no-unsanitized\n",[378,4334,4336],{"className":2095,"code":4335,"language":2097,"meta":213,"style":213},"{\n \"plugins\": [\"security\", \"no-unsanitized\"],\n \"extends\": [\"plugin:security/recommended\"],\n \"rules\": {\n \"no-unsanitized/method\": \"error\",\n \"no-unsanitized/property\": \"error\"\n }\n}\n",[287,4337,4338,4342,4359,4371,4378,4390,4400,4404],{"__ignoreMap":213},[1173,4339,4340],{"class":1175,"line":1176},[1173,4341,2104],{"class":1269},[1173,4343,4344,4347,4349,4352,4354,4357],{"class":1175,"line":217},[1173,4345,4346],{"class":1238}," \"plugins\"",[1173,4348,1321],{"class":1269},[1173,4350,4351],{"class":1189},"\"security\"",[1173,4353,1327],{"class":1269},[1173,4355,4356],{"class":1189},"\"no-unsanitized\"",[1173,4358,3789],{"class":1269},[1173,4360,4361,4364,4366,4369],{"class":1175,"line":214},[1173,4362,4363],{"class":1238}," \"extends\"",[1173,4365,1321],{"class":1269},[1173,4367,4368],{"class":1189},"\"plugin:security/recommended\"",[1173,4370,3789],{"class":1269},[1173,4372,4373,4376],{"class":1175,"line":1201},[1173,4374,4375],{"class":1238}," \"rules\"",[1173,4377,2136],{"class":1269},[1173,4379,4380,4383,4385,4388],{"class":1175,"line":1207},[1173,4381,4382],{"class":1238}," \"no-unsanitized/method\"",[1173,4384,1281],{"class":1269},[1173,4386,4387],{"class":1189},"\"error\"",[1173,4389,2116],{"class":1269},[1173,4391,4392,4395,4397],{"class":1175,"line":1222},[1173,4393,4394],{"class":1238}," \"no-unsanitized/property\"",[1173,4396,1281],{"class":1269},[1173,4398,4399],{"class":1189},"\"error\"\n",[1173,4401,4402],{"class":1175,"line":235},[1173,4403,2151],{"class":1269},[1173,4405,4406],{"class":1175,"line":1232},[1173,4407,1630],{"class":1269},[20,4409,3723,4410,4413,4414,1327,4417,4420,4421,4424,4425,4428],{},[287,4411,4412],{},"security"," plugin flags dangerous patterns like ",[287,4415,4416],{},"eval()",[287,4418,4419],{},"RegExp()"," with dynamic patterns (ReDoS vulnerability), and child process execution with unsanitized input. The ",[287,4422,4423],{},"no-unsanitized"," plugin catches uses of ",[287,4426,4427],{},"innerHTML"," and similar sinks.",[20,4430,4431,4434],{},[36,4432,4433],{},"CodeQL"," (GitHub's semantic code analysis) goes deeper than pattern matching — it builds a semantic model of your code and tracks data flow, finding tainted data flows from user input to dangerous sinks. It is more accurate than simple pattern matching and catches vulnerabilities that cross function boundaries:",[378,4436,4438],{"className":1251,"code":4437,"language":1253,"meta":213,"style":213},"- name: Initialize CodeQL\n uses: github/codeql-action/init@v3\n with:\n languages: javascript\n\n- name: Perform CodeQL Analysis\n uses: github/codeql-action/analyze@v3\n with:\n category: \"/language:javascript\"\n",[287,4439,4440,4451,4460,4466,4476,4480,4491,4500,4506],{"__ignoreMap":213},[1173,4441,4442,4444,4446,4448],{"class":1175,"line":1176},[1173,4443,1390],{"class":1269},[1173,4445,1393],{"class":1265},[1173,4447,1281],{"class":1269},[1173,4449,4450],{"class":1189},"Initialize CodeQL\n",[1173,4452,4453,4455,4457],{"class":1175,"line":217},[1173,4454,1403],{"class":1265},[1173,4456,1281],{"class":1269},[1173,4458,4459],{"class":1189},"github/codeql-action/init@v3\n",[1173,4461,4462,4464],{"class":1175,"line":214},[1173,4463,1413],{"class":1265},[1173,4465,1270],{"class":1269},[1173,4467,4468,4471,4473],{"class":1175,"line":1201},[1173,4469,4470],{"class":1265}," languages",[1173,4472,1281],{"class":1269},[1173,4474,4475],{"class":1189},"javascript\n",[1173,4477,4478],{"class":1175,"line":1207},[1173,4479,1198],{"emptyLinePlaceholder":233},[1173,4481,4482,4484,4486,4488],{"class":1175,"line":1222},[1173,4483,1390],{"class":1269},[1173,4485,1393],{"class":1265},[1173,4487,1281],{"class":1269},[1173,4489,4490],{"class":1189},"Perform CodeQL Analysis\n",[1173,4492,4493,4495,4497],{"class":1175,"line":235},[1173,4494,1403],{"class":1265},[1173,4496,1281],{"class":1269},[1173,4498,4499],{"class":1189},"github/codeql-action/analyze@v3\n",[1173,4501,4502,4504],{"class":1175,"line":1232},[1173,4503,1413],{"class":1265},[1173,4505,1270],{"class":1269},[1173,4507,4508,4511,4513],{"class":1175,"line":877},[1173,4509,4510],{"class":1265}," category",[1173,4512,1281],{"class":1269},[1173,4514,4515],{"class":1189},"\"/language:javascript\"\n",[15,4517,4519],{"id":4518},"dynamic-application-security-testing-dast","Dynamic Application Security Testing (DAST)",[20,4521,4522],{},"DAST tests your running application by sending malicious inputs and analyzing responses. It finds vulnerabilities that only manifest at runtime — configuration issues, server-side behavior that static analysis cannot see.",[20,4524,4525,4528],{},[36,4526,4527],{},"OWASP ZAP"," (Zed Attack Proxy) is the standard open-source DAST tool. Use it in two modes:",[20,4530,4531],{},"Automated scan in CI against your staging environment:",[378,4533,4535],{"className":1251,"code":4534,"language":1253,"meta":213,"style":213},"- name: ZAP Baseline Scan\n uses: zaproxy/action-baseline@v0.12.0\n with:\n target: \"https://staging.yourdomain.com\"\n rules_file_name: \".zap/rules.tsv\"\n cmd_options: \"-a\"\n",[287,4536,4537,4548,4557,4563,4572,4582],{"__ignoreMap":213},[1173,4538,4539,4541,4543,4545],{"class":1175,"line":1176},[1173,4540,1390],{"class":1269},[1173,4542,1393],{"class":1265},[1173,4544,1281],{"class":1269},[1173,4546,4547],{"class":1189},"ZAP Baseline Scan\n",[1173,4549,4550,4552,4554],{"class":1175,"line":217},[1173,4551,1403],{"class":1265},[1173,4553,1281],{"class":1269},[1173,4555,4556],{"class":1189},"zaproxy/action-baseline@v0.12.0\n",[1173,4558,4559,4561],{"class":1175,"line":214},[1173,4560,1413],{"class":1265},[1173,4562,1270],{"class":1269},[1173,4564,4565,4567,4569],{"class":1175,"line":1201},[1173,4566,1772],{"class":1265},[1173,4568,1281],{"class":1269},[1173,4570,4571],{"class":1189},"\"https://staging.yourdomain.com\"\n",[1173,4573,4574,4577,4579],{"class":1175,"line":1207},[1173,4575,4576],{"class":1265}," rules_file_name",[1173,4578,1281],{"class":1269},[1173,4580,4581],{"class":1189},"\".zap/rules.tsv\"\n",[1173,4583,4584,4587,4589],{"class":1175,"line":1222},[1173,4585,4586],{"class":1265}," cmd_options",[1173,4588,1281],{"class":1269},[1173,4590,4591],{"class":1189},"\"-a\"\n",[20,4593,4594],{},"The baseline scan runs a quick automated test covering the most common vulnerabilities — reflected XSS, SQL injection in query parameters, security headers, and misconfiguration. It produces a report you can review as part of your deployment process.",[20,4596,4597],{},"Manual scan with ZAP as a proxy: configure your browser to use ZAP as an HTTP proxy, then manually use your application. ZAP records all requests and passively scans for vulnerabilities. After your manual session, run the active scanner on the captured requests to probe each endpoint for vulnerabilities.",[20,4599,4600,4603],{},[36,4601,4602],{},"Nuclei"," is a fast, template-based scanner with a community library covering thousands of specific vulnerability patterns:",[378,4605,4607],{"className":1167,"code":4606,"language":1169,"meta":213,"style":213},"# Install\ngo install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest\n\n# Run against staging\nnuclei -u https://staging.yourdomain.com \\\n -t /nuclei-templates/http/ \\\n -tags owasp,web,exposure \\\n -severity medium,high,critical\n",[287,4608,4609,4614,4627,4631,4636,4650,4660,4670],{"__ignoreMap":213},[1173,4610,4611],{"class":1175,"line":1176},[1173,4612,4613],{"class":1179},"# Install\n",[1173,4615,4616,4619,4621,4624],{"class":1175,"line":217},[1173,4617,4618],{"class":1185},"go",[1173,4620,1190],{"class":1189},[1173,4622,4623],{"class":1238}," -v",[1173,4625,4626],{"class":1189}," github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest\n",[1173,4628,4629],{"class":1175,"line":214},[1173,4630,1198],{"emptyLinePlaceholder":233},[1173,4632,4633],{"class":1175,"line":1201},[1173,4634,4635],{"class":1179},"# Run against staging\n",[1173,4637,4638,4641,4644,4647],{"class":1175,"line":1207},[1173,4639,4640],{"class":1185},"nuclei",[1173,4642,4643],{"class":1238}," -u",[1173,4645,4646],{"class":1189}," https://staging.yourdomain.com",[1173,4648,4649],{"class":1238}," \\\n",[1173,4651,4652,4655,4658],{"class":1175,"line":1222},[1173,4653,4654],{"class":1238}," -t",[1173,4656,4657],{"class":1189}," /nuclei-templates/http/",[1173,4659,4649],{"class":1238},[1173,4661,4662,4665,4668],{"class":1175,"line":235},[1173,4663,4664],{"class":1238}," -tags",[1173,4666,4667],{"class":1189}," owasp,web,exposure",[1173,4669,4649],{"class":1238},[1173,4671,4672,4675],{"class":1175,"line":1232},[1173,4673,4674],{"class":1238}," -severity",[1173,4676,4677],{"class":1189}," medium,high,critical\n",[20,4679,4680,4681,4683],{},"Nuclei is excellent for checking for specific misconfigurations and exposure (exposed ",[287,4682,329],{}," files, debug endpoints, default credentials on admin panels).",[15,4685,4687],{"id":4686},"manual-security-testing-techniques","Manual Security Testing Techniques",[20,4689,4690],{},"Automated tools miss context-specific vulnerabilities, business logic flaws, and multi-step attack chains. Manual testing fills this gap.",[20,4692,4693,4696],{},[36,4694,4695],{},"Broken access control testing"," — the most common vulnerability class and one that automated tools frequently miss. For every API endpoint that returns data, test whether you can access other users' data by changing ID parameters. Use two test accounts and verify that Account A cannot access Account B's resources.",[20,4698,4699],{},"The test is systematic: create test accounts, log in as Account A, make note of your resource IDs, log out, log in as Account B, attempt to access Account A's resources using the IDs you noted. If it succeeds, you have broken access control.",[20,4701,4702,4705],{},[36,4703,4704],{},"Authentication bypass testing"," — test your authentication logic for edge cases. Can you access authenticated endpoints without a session cookie? With an expired token? With a token from a different environment? With a token for a deleted user?",[20,4707,4708,4711],{},[36,4709,4710],{},"Input validation fuzzing"," — send unexpected inputs to every form field and API parameter. A simple manual approach:",[185,4713,4714,4717,4720,4726,4732,4739,4742,4745],{},[188,4715,4716],{},"Send an empty string where a value is required",[188,4718,4719],{},"Send a very long string (10,000+ characters)",[188,4721,4722,4723],{},"Send SQL metacharacters: ",[287,4724,4725],{},"' \" ; -- /* */",[188,4727,4728,4729],{},"Send HTML: ",[287,4730,4731],{},"\u003Cscript>alert(1)\u003C/script>",[188,4733,4734,4735,4738],{},"Send Unicode edge cases: null bytes (",[287,4736,4737],{},"\\x00","), emoji, RTL text",[188,4740,4741],{},"Send type mismatches: a string where a number is expected, an array where a string is expected",[188,4743,4744],{},"Send negative numbers where positive is expected",[188,4746,4747],{},"Send extremely large numbers",[20,4749,4750],{},"Watch for 500 errors (indicating unhandled input), 403/401 changes (authorization behavior changes), unexpected 200 responses (input accepted when it should be rejected), or response content changes (data leakage).",[20,4752,4753,4756],{},[36,4754,4755],{},"Business logic testing"," — think about the specific workflows in your application and how they could be abused. Can a discount code be applied multiple times? Can a user cancel an order after it ships and receive a refund? Can a user upgrade their account tier without paying by manipulating a parameter? These are not generic vulnerability patterns — they require understanding your specific application.",[15,4758,4760],{"id":4759},"integrating-security-testing-into-your-workflow","Integrating Security Testing Into Your Workflow",[20,4762,4763],{},"The goal is security testing that is fast enough and automated enough that it does not create friction for developers, while still catching meaningful issues.",[20,4765,4766,4769],{},[36,4767,4768],{},"At commit time:"," ESLint security rules and pre-commit hooks run in milliseconds. Catch obvious anti-patterns before they reach a PR.",[20,4771,4772,4775],{},[36,4773,4774],{},"At PR review:"," SAST (Semgrep, CodeQL) runs on every PR via GitHub Actions. Results appear inline in the code review. This is the highest-value automated security testing because it runs on every change.",[20,4777,4778,4781],{},[36,4779,4780],{},"Pre-deployment:"," ZAP baseline scan runs against staging before each production deployment. Block deployments where new high-severity findings appear.",[20,4783,4784,4787],{},[36,4785,4786],{},"Weekly:"," Nuclei scan against staging, scheduled SAST scan with broader rule sets, dependency vulnerability scan.",[20,4789,4790,4793],{},[36,4791,4792],{},"Quarterly:"," Manual penetration testing session against the full application. This does not need to be a professional pentest — an internal security-focused code review and manual testing session by team members who understand security is valuable and much cheaper.",[20,4795,4796,4799],{},[36,4797,4798],{},"Annually:"," Professional penetration test by an external firm. Important for compliance and for catching vulnerabilities that your internal team has blind spots to. Budget this as part of your security program.",[15,4801,4803],{"id":4802},"making-sense-of-findings","Making Sense of Findings",[20,4805,4806],{},"Not every finding from automated tools is a real vulnerability. False positives are common in SAST and require human judgment to evaluate. For each finding:",[2070,4808,4809,4812,4815,4818],{},[188,4810,4811],{},"Understand what the tool thinks the vulnerability is",[188,4813,4814],{},"Trace the code path to understand the actual risk",[188,4816,4817],{},"Determine if it is exploitable in your application's context",[188,4819,4820],{},"Fix it or document why you accepted the risk",[20,4822,4823],{},"Build a security findings registry. Every finding gets a status (open, in progress, accepted risk, false positive) and a rationale. This creates accountability and ensures findings do not disappear into a backlog and get forgotten.",[27,4825],{},[20,4827,4828,4829,177],{},"If you want help setting up a security testing program for your application or want a professional security review, book a session at ",[171,4830,173],{"href":173,"rel":4831},[175],[27,4833],{},[15,4835,183],{"id":182},[185,4837,4838,4842,4848,4852],{},[188,4839,4840],{},[171,4841,3430],{"href":4165},[188,4843,4844],{},[171,4845,4847],{"href":4846},"/blog/web-security-fundamentals","Web Security Fundamentals Every Developer Should Know",[188,4849,4850],{},[171,4851,4135],{"href":4134},[188,4853,4854],{},[171,4855,3385],{"href":3384},[1971,4857,4858],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":213,"searchDepth":214,"depth":214,"links":4860},[4861,4862,4863,4864,4865,4866],{"id":4189,"depth":217,"text":4190},{"id":4518,"depth":217,"text":4519},{"id":4686,"depth":217,"text":4687},{"id":4759,"depth":217,"text":4760},{"id":4802,"depth":217,"text":4803},{"id":182,"depth":217,"text":183},"A practical guide to security testing web applications — SAST, DAST, manual testing techniques, tools, and building security testing into your development workflow.",[4869,4870],"security testing","web application testing",{},{"title":4129,"description":4867},"blog/security-testing-web-apps",[4875,4876,4877,4171],"Security Testing","DAST","SAST","MOhuz4jAvyqluMf5KHDCaHs87RWrKLNf-mgyyuRJV0w",{"id":4880,"title":4881,"author":4882,"body":4883,"category":1983,"date":224,"description":5693,"extension":226,"featured":227,"image":228,"keywords":5694,"meta":5697,"navigation":233,"path":5698,"readTime":235,"seo":5699,"stem":5700,"tags":5701,"__hash__":5705},"blog/blog/server-security-hardening.md","Server Security Hardening: The Checklist I Run on Every New VPS",{"name":9,"bio":10},{"type":12,"value":4884,"toc":5680},[4885,4888,4891,4894,4898,4901,4951,4961,4967,4970,4974,4977,5003,5006,5028,5039,5043,5046,5052,5058,5061,5087,5090,5094,5097,5228,5235,5238,5289,5293,5296,5323,5328,5384,5387,5410,5416,5420,5423,5436,5439,5486,5489,5493,5499,5505,5511,5515,5518,5521,5553,5559,5565,5568,5572,5575,5606,5609,5628,5631,5635,5642,5645,5647,5653,5655,5657,5677],[1125,4886,4881],{"id":4887},"server-security-hardening-the-checklist-i-run-on-every-new-vps",[20,4889,4890],{},"Every new VPS I spin up gets hardened within the first thirty minutes of existence. This is not paranoia — it is operational hygiene. A fresh Ubuntu server on a public IP is being port-scanned within seconds of assignment. Automated bots are attempting SSH authentication within minutes. Skipping basic hardening is not a risk you defer; it is a risk you accept immediately.",[20,4892,4893],{},"Here is the exact checklist, in order, with the commands.",[15,4895,4897],{"id":4896},"step-1-system-updates","Step 1: System Updates",[20,4899,4900],{},"Before anything else, update all packages:",[378,4902,4904],{"className":1167,"code":4903,"language":1169,"meta":213,"style":213},"apt update && apt upgrade -y\napt install -y unattended-upgrades apt-listchanges\ndpkg-reconfigure -plow unattended-upgrades\n",[287,4905,4906,4925,4940],{"__ignoreMap":213},[1173,4907,4908,4911,4914,4917,4919,4922],{"class":1175,"line":1176},[1173,4909,4910],{"class":1185},"apt",[1173,4912,4913],{"class":1189}," update",[1173,4915,4916],{"class":1269}," && ",[1173,4918,4910],{"class":1185},[1173,4920,4921],{"class":1189}," upgrade",[1173,4923,4924],{"class":1238}," -y\n",[1173,4926,4927,4929,4931,4934,4937],{"class":1175,"line":217},[1173,4928,4910],{"class":1185},[1173,4930,1190],{"class":1189},[1173,4932,4933],{"class":1238}," -y",[1173,4935,4936],{"class":1189}," unattended-upgrades",[1173,4938,4939],{"class":1189}," apt-listchanges\n",[1173,4941,4942,4945,4948],{"class":1175,"line":214},[1173,4943,4944],{"class":1185},"dpkg-reconfigure",[1173,4946,4947],{"class":1238}," -plow",[1173,4949,4950],{"class":1189}," unattended-upgrades\n",[20,4952,3723,4953,4956,4957,4960],{},[287,4954,4955],{},"unattended-upgrades"," package automatically applies security updates. Enable it. Configure automatic reboot in ",[287,4958,4959],{},"/etc/apt/apt.conf.d/50unattended-upgrades"," if your application can handle periodic restarts:",[378,4962,4965],{"className":4963,"code":4964,"language":383},[381],"Unattended-Upgrade::Automatic-Reboot \"true\";\nUnattended-Upgrade::Automatic-Reboot-Time \"02:00\";\n",[287,4966,4964],{"__ignoreMap":213},[20,4968,4969],{},"Security patches that require a reboot will apply automatically at 2am. For applications that need zero-downtime patching, this needs more thought — but for most single-server deployments, it is the right tradeoff.",[15,4971,4973],{"id":4972},"step-2-create-a-non-root-user","Step 2: Create a Non-Root User",[20,4975,4976],{},"Never operate as root. Create a dedicated admin user:",[378,4978,4980],{"className":1167,"code":4979,"language":1169,"meta":213,"style":213},"adduser james\nusermod -aG sudo james\n",[287,4981,4982,4990],{"__ignoreMap":213},[1173,4983,4984,4987],{"class":1175,"line":1176},[1173,4985,4986],{"class":1185},"adduser",[1173,4988,4989],{"class":1189}," james\n",[1173,4991,4992,4995,4998,5001],{"class":1175,"line":217},[1173,4993,4994],{"class":1185},"usermod",[1173,4996,4997],{"class":1238}," -aG",[1173,4999,5000],{"class":1189}," sudo",[1173,5002,4989],{"class":1189},[20,5004,5005],{},"Copy your SSH keys to the new user:",[378,5007,5009],{"className":1167,"code":5008,"language":1169,"meta":213,"style":213},"rsync --archive --chown=james:james ~/.ssh /home/james\n",[287,5010,5011],{"__ignoreMap":213},[1173,5012,5013,5016,5019,5022,5025],{"class":1175,"line":1176},[1173,5014,5015],{"class":1185},"rsync",[1173,5017,5018],{"class":1238}," --archive",[1173,5020,5021],{"class":1238}," --chown=james:james",[1173,5023,5024],{"class":1189}," ~/.ssh",[1173,5026,5027],{"class":1189}," /home/james\n",[20,5029,5030,5031,5034,5035,5038],{},"From this point, all work happens as the ",[287,5032,5033],{},"james"," user with ",[287,5036,5037],{},"sudo"," when needed.",[15,5040,5042],{"id":5041},"step-3-harden-ssh","Step 3: Harden SSH",[20,5044,5045],{},"This is the highest-leverage security step. The default SSH configuration accepts password authentication, which means the bot attacks scanning port 22 can potentially brute-force your server if you use a weak password.",[20,5047,5048,5049,1527],{},"Edit ",[287,5050,5051],{},"/etc/ssh/sshd_config",[378,5053,5056],{"className":5054,"code":5055,"language":383},[381],"# Disable root login\nPermitRootLogin no\n\n# Disable password authentication - require SSH keys\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Disable X11 forwarding if not needed\nX11Forwarding no\n\n# Disable empty passwords\nPermitEmptyPasswords no\n\n# Set max authentication attempts\nMaxAuthTries 3\n\n# Limit SSH access to your user\nAllowUsers james\n\n# Change port (optional - reduces log noise but is not true security)\n# Port 2222\n\n# Set idle timeout\nClientAliveInterval 300\nClientAliveCountMax 2\n",[287,5057,5055],{"__ignoreMap":213},[20,5059,5060],{},"Verify your SSH key works before restarting sshd. Open a second terminal and test login before closing your current session.",[378,5062,5064],{"className":1167,"code":5063,"language":1169,"meta":213,"style":213},"sshd -t # Syntax check\nsystemctl restart sshd\n",[287,5065,5066,5076],{"__ignoreMap":213},[1173,5067,5068,5071,5073],{"class":1175,"line":1176},[1173,5069,5070],{"class":1185},"sshd",[1173,5072,4654],{"class":1238},[1173,5074,5075],{"class":1179}," # Syntax check\n",[1173,5077,5078,5081,5084],{"class":1175,"line":217},[1173,5079,5080],{"class":1185},"systemctl",[1173,5082,5083],{"class":1189}," restart",[1173,5085,5086],{"class":1189}," sshd\n",[20,5088,5089],{},"If you change the SSH port, update your firewall rule before restarting. Getting locked out of a VPS is recoverable (most providers offer console access) but annoying.",[15,5091,5093],{"id":5092},"step-4-configure-ufw-firewall","Step 4: Configure UFW Firewall",[20,5095,5096],{},"UFW (Uncomplicated Firewall) is a front-end for iptables that makes firewall rules manageable:",[378,5098,5100],{"className":1167,"code":5099,"language":1169,"meta":213,"style":213},"apt install -y ufw\n\n# Default: deny incoming, allow outgoing\nufw default deny incoming\nufw default allow outgoing\n\n# Allow SSH (use 2222 if you changed the port)\nufw allow 22/tcp\n\n# Allow HTTP and HTTPS\nufw allow 80/tcp\nufw allow 443/tcp\n\n# Enable the firewall\nufw enable\n\n# Verify rules\nufw status verbose\n",[287,5101,5102,5113,5117,5122,5136,5148,5152,5157,5166,5170,5175,5184,5193,5197,5202,5209,5213,5218],{"__ignoreMap":213},[1173,5103,5104,5106,5108,5110],{"class":1175,"line":1176},[1173,5105,4910],{"class":1185},[1173,5107,1190],{"class":1189},[1173,5109,4933],{"class":1238},[1173,5111,5112],{"class":1189}," ufw\n",[1173,5114,5115],{"class":1175,"line":217},[1173,5116,1198],{"emptyLinePlaceholder":233},[1173,5118,5119],{"class":1175,"line":214},[1173,5120,5121],{"class":1179},"# Default: deny incoming, allow outgoing\n",[1173,5123,5124,5127,5130,5133],{"class":1175,"line":1201},[1173,5125,5126],{"class":1185},"ufw",[1173,5128,5129],{"class":1189}," default",[1173,5131,5132],{"class":1189}," deny",[1173,5134,5135],{"class":1189}," incoming\n",[1173,5137,5138,5140,5142,5145],{"class":1175,"line":1207},[1173,5139,5126],{"class":1185},[1173,5141,5129],{"class":1189},[1173,5143,5144],{"class":1189}," allow",[1173,5146,5147],{"class":1189}," outgoing\n",[1173,5149,5150],{"class":1175,"line":1222},[1173,5151,1198],{"emptyLinePlaceholder":233},[1173,5153,5154],{"class":1175,"line":235},[1173,5155,5156],{"class":1179},"# Allow SSH (use 2222 if you changed the port)\n",[1173,5158,5159,5161,5163],{"class":1175,"line":1232},[1173,5160,5126],{"class":1185},[1173,5162,5144],{"class":1189},[1173,5164,5165],{"class":1189}," 22/tcp\n",[1173,5167,5168],{"class":1175,"line":877},[1173,5169,1198],{"emptyLinePlaceholder":233},[1173,5171,5172],{"class":1175,"line":1351},[1173,5173,5174],{"class":1179},"# Allow HTTP and HTTPS\n",[1173,5176,5177,5179,5181],{"class":1175,"line":1361},[1173,5178,5126],{"class":1185},[1173,5180,5144],{"class":1189},[1173,5182,5183],{"class":1189}," 80/tcp\n",[1173,5185,5186,5188,5190],{"class":1175,"line":1368},[1173,5187,5126],{"class":1185},[1173,5189,5144],{"class":1189},[1173,5191,5192],{"class":1189}," 443/tcp\n",[1173,5194,5195],{"class":1175,"line":1785},[1173,5196,1198],{"emptyLinePlaceholder":233},[1173,5198,5199],{"class":1175,"line":1796},[1173,5200,5201],{"class":1179},"# Enable the firewall\n",[1173,5203,5204,5206],{"class":1175,"line":1804},[1173,5205,5126],{"class":1185},[1173,5207,5208],{"class":1189}," enable\n",[1173,5210,5211],{"class":1175,"line":1817},[1173,5212,1198],{"emptyLinePlaceholder":233},[1173,5214,5215],{"class":1175,"line":1825},[1173,5216,5217],{"class":1179},"# Verify rules\n",[1173,5219,5220,5222,5225],{"class":1175,"line":2347},[1173,5221,5126],{"class":1185},[1173,5223,5224],{"class":1189}," status",[1173,5226,5227],{"class":1189}," verbose\n",[20,5229,5230,5231,5234],{},"If you changed your SSH port, allow that port before enabling UFW — ",[287,5232,5233],{},"ufw enable"," with SSH blocked is another way to lock yourself out.",[20,5236,5237],{},"For servers that should only be accessed from specific IP ranges (an internal API server, for example), restrict access:",[378,5239,5241],{"className":1167,"code":5240,"language":1169,"meta":213,"style":213},"# Allow SSH only from your office IP\nufw allow from 203.0.113.50 to any port 22\n\n# Deny SSH from everywhere else\nufw deny 22/tcp\n",[287,5242,5243,5248,5272,5276,5281],{"__ignoreMap":213},[1173,5244,5245],{"class":1175,"line":1176},[1173,5246,5247],{"class":1179},"# Allow SSH only from your office IP\n",[1173,5249,5250,5252,5254,5257,5260,5263,5266,5269],{"class":1175,"line":217},[1173,5251,5126],{"class":1185},[1173,5253,5144],{"class":1189},[1173,5255,5256],{"class":1189}," from",[1173,5258,5259],{"class":1238}," 203.0.113.50",[1173,5261,5262],{"class":1189}," to",[1173,5264,5265],{"class":1189}," any",[1173,5267,5268],{"class":1189}," port",[1173,5270,5271],{"class":1238}," 22\n",[1173,5273,5274],{"class":1175,"line":214},[1173,5275,1198],{"emptyLinePlaceholder":233},[1173,5277,5278],{"class":1175,"line":1201},[1173,5279,5280],{"class":1179},"# Deny SSH from everywhere else\n",[1173,5282,5283,5285,5287],{"class":1175,"line":1207},[1173,5284,5126],{"class":1185},[1173,5286,5132],{"class":1189},[1173,5288,5165],{"class":1189},[15,5290,5292],{"id":5291},"step-5-fail2ban","Step 5: Fail2Ban",[20,5294,5295],{},"Fail2Ban monitors log files for repeated authentication failures and automatically bans the offending IP via firewall rules. It dramatically reduces brute-force attack surface:",[378,5297,5299],{"className":1167,"code":5298,"language":1169,"meta":213,"style":213},"apt install -y fail2ban\ncp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\n",[287,5300,5301,5312],{"__ignoreMap":213},[1173,5302,5303,5305,5307,5309],{"class":1175,"line":1176},[1173,5304,4910],{"class":1185},[1173,5306,1190],{"class":1189},[1173,5308,4933],{"class":1238},[1173,5310,5311],{"class":1189}," fail2ban\n",[1173,5313,5314,5317,5320],{"class":1175,"line":217},[1173,5315,5316],{"class":1185},"cp",[1173,5318,5319],{"class":1189}," /etc/fail2ban/jail.conf",[1173,5321,5322],{"class":1189}," /etc/fail2ban/jail.local\n",[20,5324,5048,5325,1527],{},[287,5326,5327],{},"/etc/fail2ban/jail.local",[378,5329,5333],{"className":5330,"code":5331,"language":5332,"meta":213,"style":213},"language-ini shiki shiki-themes github-dark","[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\n\n[sshd]\nenabled = true\nport = 22\nmaxretry = 3\nbantime = 86400\n","ini",[287,5334,5335,5340,5345,5350,5355,5359,5364,5369,5374,5379],{"__ignoreMap":213},[1173,5336,5337],{"class":1175,"line":1176},[1173,5338,5339],{},"[DEFAULT]\n",[1173,5341,5342],{"class":1175,"line":217},[1173,5343,5344],{},"bantime = 3600\n",[1173,5346,5347],{"class":1175,"line":214},[1173,5348,5349],{},"findtime = 600\n",[1173,5351,5352],{"class":1175,"line":1201},[1173,5353,5354],{},"maxretry = 5\n",[1173,5356,5357],{"class":1175,"line":1207},[1173,5358,1198],{"emptyLinePlaceholder":233},[1173,5360,5361],{"class":1175,"line":1222},[1173,5362,5363],{},"[sshd]\n",[1173,5365,5366],{"class":1175,"line":235},[1173,5367,5368],{},"enabled = true\n",[1173,5370,5371],{"class":1175,"line":1232},[1173,5372,5373],{},"port = 22\n",[1173,5375,5376],{"class":1175,"line":877},[1173,5377,5378],{},"maxretry = 3\n",[1173,5380,5381],{"class":1175,"line":1351},[1173,5382,5383],{},"bantime = 86400\n",[20,5385,5386],{},"This bans IPs that fail SSH authentication 3 times within 10 minutes, for 24 hours. Start Fail2Ban:",[378,5388,5390],{"className":1167,"code":5389,"language":1169,"meta":213,"style":213},"systemctl enable fail2ban\nsystemctl start fail2ban\n",[287,5391,5392,5401],{"__ignoreMap":213},[1173,5393,5394,5396,5399],{"class":1175,"line":1176},[1173,5395,5080],{"class":1185},[1173,5397,5398],{"class":1189}," enable",[1173,5400,5311],{"class":1189},[1173,5402,5403,5405,5408],{"class":1175,"line":217},[1173,5404,5080],{"class":1185},[1173,5406,5407],{"class":1189}," start",[1173,5409,5311],{"class":1189},[20,5411,5412,5413],{},"Check ban status: ",[287,5414,5415],{},"fail2ban-client status sshd",[15,5417,5419],{"id":5418},"step-6-disable-unused-services","Step 6: Disable Unused Services",[20,5421,5422],{},"Every running service is an attack surface. Check what is listening:",[378,5424,5426],{"className":1167,"code":5425,"language":1169,"meta":213,"style":213},"ss -tlnp\n",[287,5427,5428],{"__ignoreMap":213},[1173,5429,5430,5433],{"class":1175,"line":1176},[1173,5431,5432],{"class":1185},"ss",[1173,5434,5435],{"class":1238}," -tlnp\n",[20,5437,5438],{},"Review each open port. Disable services you do not need. On a fresh Ubuntu install, common candidates to disable:",[378,5440,5442],{"className":1167,"code":5441,"language":1169,"meta":213,"style":213},"# Disable postfix if you're not using the server as a mail relay\nsystemctl disable postfix\nsystemctl stop postfix\n\n# Disable snapd if you don't use snap packages\nsystemctl disable snapd\n",[287,5443,5444,5449,5459,5468,5472,5477],{"__ignoreMap":213},[1173,5445,5446],{"class":1175,"line":1176},[1173,5447,5448],{"class":1179},"# Disable postfix if you're not using the server as a mail relay\n",[1173,5450,5451,5453,5456],{"class":1175,"line":217},[1173,5452,5080],{"class":1185},[1173,5454,5455],{"class":1189}," disable",[1173,5457,5458],{"class":1189}," postfix\n",[1173,5460,5461,5463,5466],{"class":1175,"line":214},[1173,5462,5080],{"class":1185},[1173,5464,5465],{"class":1189}," stop",[1173,5467,5458],{"class":1189},[1173,5469,5470],{"class":1175,"line":1201},[1173,5471,1198],{"emptyLinePlaceholder":233},[1173,5473,5474],{"class":1175,"line":1207},[1173,5475,5476],{"class":1179},"# Disable snapd if you don't use snap packages\n",[1173,5478,5479,5481,5483],{"class":1175,"line":1222},[1173,5480,5080],{"class":1185},[1173,5482,5455],{"class":1189},[1173,5484,5485],{"class":1189}," snapd\n",[20,5487,5488],{},"The goal is minimum viable attack surface. Every port that is not open is a port that cannot be exploited.",[15,5490,5492],{"id":5491},"step-7-kernel-hardening-via-sysctl","Step 7: Kernel Hardening via sysctl",[20,5494,5495,5496,1527],{},"A few kernel parameters that improve security. Add these to ",[287,5497,5498],{},"/etc/sysctl.d/99-security.conf",[378,5500,5503],{"className":5501,"code":5502,"language":383},[381],"# Ignore ICMP broadcast requests (Smurf attack prevention)\nnet.ipv4.icmp_echo_ignore_broadcasts = 1\n\n# Disable source routing\nnet.ipv4.conf.all.accept_source_route = 0\nnet.ipv6.conf.all.accept_source_route = 0\n\n# Enable SYN flood protection\nnet.ipv4.tcp_syncookies = 1\n\n# Log martian packets (spoofed source addresses)\nnet.ipv4.conf.all.log_martians = 1\n\n# Disable ICMP redirect acceptance\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv6.conf.all.accept_redirects = 0\n\n# Protect against time-wait assassination\nnet.ipv4.tcp_rfc1337 = 1\n",[287,5504,5502],{"__ignoreMap":213},[20,5506,5507,5508],{},"Apply: ",[287,5509,5510],{},"sysctl -p /etc/sysctl.d/99-security.conf",[15,5512,5514],{"id":5513},"step-8-audit-logging","Step 8: Audit Logging",[20,5516,5517],{},"Ensure system events are being logged and logs are sent offsite. Logs stored only on the compromised server are meaningless — an attacker with root access can delete them.",[20,5519,5520],{},"Install auditd for kernel-level audit logging:",[378,5522,5524],{"className":1167,"code":5523,"language":1169,"meta":213,"style":213},"apt install -y auditd\nsystemctl enable auditd\nsystemctl start auditd\n",[287,5525,5526,5537,5545],{"__ignoreMap":213},[1173,5527,5528,5530,5532,5534],{"class":1175,"line":1176},[1173,5529,4910],{"class":1185},[1173,5531,1190],{"class":1189},[1173,5533,4933],{"class":1238},[1173,5535,5536],{"class":1189}," auditd\n",[1173,5538,5539,5541,5543],{"class":1175,"line":217},[1173,5540,5080],{"class":1185},[1173,5542,5398],{"class":1189},[1173,5544,5536],{"class":1189},[1173,5546,5547,5549,5551],{"class":1175,"line":214},[1173,5548,5080],{"class":1185},[1173,5550,5407],{"class":1189},[1173,5552,5536],{"class":1189},[20,5554,5555,5556,1527],{},"Configure audit rules in ",[287,5557,5558],{},"/etc/audit/rules.d/audit.rules",[378,5560,5563],{"className":5561,"code":5562,"language":383},[381],"# Log authentication events\n-w /var/log/auth.log -p rwxa -k auth\n\n# Log user/group changes\n-w /etc/passwd -p wa -k identity\n-w /etc/group -p wa -k identity\n-w /etc/shadow -p wa -k identity\n\n# Log sudo usage\n-w /var/log/sudo.log -p w -k sudo\n\n# Log SSH key changes\n-w /home -p w -k ssh_keys\n",[287,5564,5562],{"__ignoreMap":213},[20,5566,5567],{},"Ship logs offsite to a SIEM or at minimum a log management service. Your auth logs on a compromised server cannot be trusted.",[15,5569,5571],{"id":5570},"step-9-intrusion-detection-with-aide","Step 9: Intrusion Detection with AIDE",[20,5573,5574],{},"AIDE (Advanced Intrusion Detection Environment) creates a database of file hashes for your system and alerts when files change unexpectedly. This detects rootkits and post-exploitation file modifications:",[378,5576,5578],{"className":1167,"code":5577,"language":1169,"meta":213,"style":213},"apt install -y aide\naideinit\ncp /var/lib/aide/aide.db.new /var/lib/aide/aide.db\n",[287,5579,5580,5591,5596],{"__ignoreMap":213},[1173,5581,5582,5584,5586,5588],{"class":1175,"line":1176},[1173,5583,4910],{"class":1185},[1173,5585,1190],{"class":1189},[1173,5587,4933],{"class":1238},[1173,5589,5590],{"class":1189}," aide\n",[1173,5592,5593],{"class":1175,"line":217},[1173,5594,5595],{"class":1185},"aideinit\n",[1173,5597,5598,5600,5603],{"class":1175,"line":214},[1173,5599,5316],{"class":1185},[1173,5601,5602],{"class":1189}," /var/lib/aide/aide.db.new",[1173,5604,5605],{"class":1189}," /var/lib/aide/aide.db\n",[20,5607,5608],{},"Run checks daily via cron:",[378,5610,5612],{"className":1167,"code":5611,"language":1169,"meta":213,"style":213},"echo \"0 4 * * * root /usr/bin/aide --check | mail -s 'AIDE Report' admin@yourdomain.com\" >> /etc/crontab\n",[287,5613,5614],{"__ignoreMap":213},[1173,5615,5616,5619,5622,5625],{"class":1175,"line":1176},[1173,5617,5618],{"class":1238},"echo",[1173,5620,5621],{"class":1189}," \"0 4 * * * root /usr/bin/aide --check | mail -s 'AIDE Report' admin@yourdomain.com\"",[1173,5623,5624],{"class":1215}," >>",[1173,5626,5627],{"class":1189}," /etc/crontab\n",[20,5629,5630],{},"The first few runs require tuning to mark expected changes (package updates, log rotations) as noise. After that, unexpected changes in system files are a serious alert.",[15,5632,5634],{"id":5633},"the-ongoing-maintenance","The Ongoing Maintenance",[20,5636,5637,5638,5641],{},"Hardening is not a one-time event. Run ",[287,5639,5640],{},"apt upgrade"," regularly (or let unattended-upgrades handle it). Review Fail2Ban logs weekly. Audit active user accounts and SSH authorized keys quarterly — remove access for anyone who no longer needs it. When a vulnerability is announced in software you run, patch within 24 hours for critical severity.",[20,5643,5644],{},"Security is operational discipline, not a configuration you set and forget.",[27,5646],{},[20,5648,5649,5650,177],{},"If you want a security review of your server infrastructure or help setting up a hardening baseline for your team, book a session at ",[171,5651,173],{"href":173,"rel":5652},[175],[27,5654],{},[15,5656,183],{"id":182},[185,5658,5659,5663,5667,5671],{},[188,5660,5661],{},[171,5662,1951],{"href":1950},[188,5664,5665],{},[171,5666,1963],{"href":1962},[188,5668,5669],{},[171,5670,1969],{"href":1968},[188,5672,5673],{},[171,5674,5676],{"href":5675},"/blog/cloud-cost-optimization","Cloud Cost Optimization: Cutting the Bill Without Cutting Corners",[1971,5678,5679],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}",{"title":213,"searchDepth":214,"depth":214,"links":5681},[5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692],{"id":4896,"depth":217,"text":4897},{"id":4972,"depth":217,"text":4973},{"id":5041,"depth":217,"text":5042},{"id":5092,"depth":217,"text":5093},{"id":5291,"depth":217,"text":5292},{"id":5418,"depth":217,"text":5419},{"id":5491,"depth":217,"text":5492},{"id":5513,"depth":217,"text":5514},{"id":5570,"depth":217,"text":5571},{"id":5633,"depth":217,"text":5634},{"id":182,"depth":217,"text":183},"The exact server security hardening checklist for new Linux VPS deployments — SSH hardening, firewall setup, automatic updates, and intrusion detection basics.",[5695,5696],"server security hardening","Linux server security",{},"/blog/server-security-hardening",{"title":4881,"description":5693},"blog/server-security-hardening",[5702,5703,1983,5704],"Server Security","Linux","Hardening","sOWb188NHPwYM2SwrxE4i5k4qd9aMnnXu7mEnxNn0Ms",{"id":5707,"title":5708,"author":5709,"body":5710,"category":884,"date":224,"description":5969,"extension":226,"featured":227,"image":228,"keywords":5970,"meta":5976,"navigation":233,"path":5977,"readTime":877,"seo":5978,"stem":5979,"tags":5980,"__hash__":5985},"blog/blog/serverless-architecture-guide.md","Serverless Architecture: When to Go Functions-First",{"name":9,"bio":10},{"type":12,"value":5711,"toc":5948},[5712,5716,5719,5722,5725,5727,5731,5734,5740,5746,5749,5751,5755,5760,5763,5766,5770,5773,5776,5780,5783,5787,5790,5792,5796,5800,5803,5806,5809,5813,5816,5819,5823,5826,5830,5833,5837,5840,5842,5846,5849,5852,5855,5858,5860,5864,5867,5873,5879,5885,5891,5897,5899,5903,5906,5909,5911,5918,5920,5922],[15,5713,5715],{"id":5714},"the-premise-and-the-reality","The Premise and the Reality",[20,5717,5718],{},"Serverless architecture's core promise is compelling: write a function, deploy it, and never think about servers, scaling, or infrastructure management. The cloud provider handles provisioning, scaling, patching, and availability. You pay only for the compute you actually use.",[20,5720,5721],{},"For the right workloads, this promise holds. Serverless is genuinely powerful for event-driven processing, unpredictable spikes in traffic, lightweight APIs, and workflows that run infrequently. For the wrong workloads, the costs and operational complexity are significantly higher than they appear in marketing materials.",[20,5723,5724],{},"The goal of this post is to give you a clear framework for when serverless is the right architectural choice and when it will create more problems than it solves.",[27,5726],{},[15,5728,5730],{"id":5729},"what-serverless-actually-means","What \"Serverless\" Actually Means",[20,5732,5733],{},"When developers say \"serverless,\" they typically mean one of two things:",[20,5735,5736,5739],{},[36,5737,5738],{},"Function-as-a-Service (FaaS):"," Individual functions deployed and executed in ephemeral compute containers. AWS Lambda, Google Cloud Functions, Cloudflare Workers, Azure Functions. You write a function that handles a request or event. The platform spins up compute to run it, runs it, and tears down the compute when done.",[20,5741,5742,5745],{},[36,5743,5744],{},"Managed backends:"," Services like DynamoDB, Firebase, Auth0, or Vercel's managed infrastructure where you consume infrastructure capabilities without managing the underlying servers. These are adjacent to serverless FaaS but involve different architectural considerations.",[20,5747,5748],{},"This post focuses primarily on FaaS, since that's where the most interesting architectural trade-offs live.",[27,5750],{},[15,5752,5754],{"id":5753},"where-serverless-genuinely-wins","Where Serverless Genuinely Wins",[5756,5757,5759],"h3",{"id":5758},"infrequent-or-unpredictable-traffic","Infrequent or Unpredictable Traffic",[20,5761,5762],{},"If your workload runs occasionally or experiences unpredictable spikes — a webhook handler that receives events when a payment is processed, a report generator that runs on-demand, a data processing job triggered by file uploads — serverless is a strong fit.",[20,5764,5765],{},"You're not paying for idle compute. A Lambda function that runs 1,000 times per month costs nearly nothing compared to a container that's running 24/7 waiting for those 1,000 invocations.",[5756,5767,5769],{"id":5768},"event-driven-processing-pipelines","Event-Driven Processing Pipelines",[20,5771,5772],{},"Serverless functions are natural consumers for event streams and queue messages. A function triggered by an S3 object creation, an SQS message, or a DynamoDB stream can process events at scale without managing consumer infrastructure. The platform handles concurrency automatically — if you get 10,000 S3 events simultaneously, the platform spins up 10,000 function instances.",[20,5774,5775],{},"This auto-scaling is genuinely powerful for event processing workloads where the event rate is unpredictable.",[5756,5777,5779],{"id":5778},"edge-computing","Edge Computing",[20,5781,5782],{},"Cloudflare Workers and similar edge function platforms execute at the network edge — in data centers close to users globally. For use cases where latency matters and the compute is lightweight (authentication checks, request routing, A/B testing, content transformation), edge functions provide performance that regional container deployments can't match.",[5756,5784,5786],{"id":5785},"lightweight-apis-and-webhooks","Lightweight APIs and Webhooks",[20,5788,5789],{},"A simple API with low-to-moderate traffic and no long-running connections is a good serverless candidate. The concurrency model handles scaling automatically, and the cost model is favorable at modest traffic levels.",[27,5791],{},[15,5793,5795],{"id":5794},"where-serverless-struggles","Where Serverless Struggles",[5756,5797,5799],{"id":5798},"cold-starts","Cold Starts",[20,5801,5802],{},"When a function hasn't been invoked recently, the platform needs to initialize a new compute environment before executing it. This initialization time — the cold start — adds latency that can be significant: 200ms to several seconds depending on the runtime, memory configuration, and deployment package size.",[20,5804,5805],{},"For user-facing APIs where response time matters, cold starts create a variable latency tail that's difficult to eliminate entirely. You can mitigate them with provisioned concurrency (keeping warm instances available) but that adds cost and reduces the pay-per-use benefit.",[20,5807,5808],{},"Java and .NET runtimes have substantially longer cold starts than Node.js, Python, or Go. If cold start latency is critical, your language choice is constrained.",[5756,5810,5812],{"id":5811},"long-running-processes","Long-Running Processes",[20,5814,5815],{},"Serverless functions have execution time limits. AWS Lambda's maximum is 15 minutes. Google Cloud Functions allows 60 minutes. If your workload needs to run longer than the platform limit, serverless isn't the right model.",[20,5817,5818],{},"More importantly, long-running functions hold open a compute context that you're paying for. A function that runs for 10 minutes processing a large dataset is often more expensive than a container that does the same work.",[5756,5820,5822],{"id":5821},"stateful-workloads","Stateful Workloads",[20,5824,5825],{},"Functions are ephemeral. There's no in-process state between invocations. State must live in an external store: a database, a cache, or a managed service. This is actually good architectural discipline in general, but for workloads that naturally need in-process state (WebSocket connections, streaming processing, stateful workflows), it creates friction.",[5756,5827,5829],{"id":5828},"high-throughput-latency-sensitive-apis","High-Throughput, Latency-Sensitive APIs",[20,5831,5832],{},"At very high traffic volumes (millions of requests per day with strict SLAs), the economics of serverless often shift. A well-tuned container cluster can be cheaper than per-invocation pricing at sufficient scale. Calculate both options rather than assuming serverless is cheaper.",[5756,5834,5836],{"id":5835},"vendor-lock-in","Vendor Lock-In",[20,5838,5839],{},"Lambda functions with AWS-specific event sources, IAM roles, and environment configuration are not trivially portable. Writing truly portable serverless code requires abstractions that add complexity. Most teams accept some level of vendor coupling; it's worth acknowledging the trade-off explicitly.",[27,5841],{},[15,5843,5845],{"id":5844},"the-cost-model-understand-it-before-you-commit","The Cost Model: Understand It Before You Commit",[20,5847,5848],{},"Serverless pricing has two primary components: invocation count and compute-time (GB-seconds). At low volumes, this is almost free. At high volumes, the math changes.",[20,5850,5851],{},"A rough comparison point: a single AWS Lambda function receiving 10 million requests per month with 200ms average duration and 512MB memory costs approximately $20-30/month. A t3.small EC2 instance running continuously costs about $17/month. At 10 million requests per month, the economics are comparable. At 100 million requests, the container is likely cheaper.",[20,5853,5854],{},"For unpredictable or low-volume workloads, serverless wins on cost. For predictable high-volume workloads, the comparison isn't as obvious.",[20,5856,5857],{},"Also account for the \"hidden\" costs: API Gateway or similar request routing adds per-request cost, provisioned concurrency for cold start mitigation adds a constant charge, and operational tooling for distributed function fleets has overhead.",[27,5859],{},[15,5861,5863],{"id":5862},"architectural-patterns-for-serverless","Architectural Patterns for Serverless",[20,5865,5866],{},"When serverless is the right choice, a few patterns help manage the challenges:",[20,5868,5869,5872],{},[36,5870,5871],{},"Function composition over orchestration."," Prefer functions that do one thing and chain through events or queues. Avoid orchestrating long chains of synchronous function calls — the error handling complexity compounds quickly.",[20,5874,5875,5878],{},[36,5876,5877],{},"Use Step Functions (or equivalent) for complex workflows."," AWS Step Functions provides stateful workflow orchestration for multi-step serverless processes, handling retries, error handling, and timeouts in a managed way.",[20,5880,5881,5884],{},[36,5882,5883],{},"Warm-up strategies."," For latency-sensitive functions, provisioned concurrency or scheduled \"ping\" invocations can keep instances warm. Understand the cost trade-off.",[20,5886,5887,5890],{},[36,5888,5889],{},"Package size discipline."," Larger deployment packages mean longer cold starts. Tree-shake dependencies aggressively. Deploy only what the function needs.",[20,5892,5893,5896],{},[36,5894,5895],{},"Observability from the start."," Distributed function fleets are harder to observe than monolithic services. Invest in distributed tracing (AWS X-Ray, Honeycomb) before you need to debug a production issue.",[27,5898],{},[15,5900,5902],{"id":5901},"the-honest-summary","The Honest Summary",[20,5904,5905],{},"Serverless is not a universal replacement for containers and VMs. It's a specialized compute model that's genuinely excellent for event-driven processing, infrequent workloads, edge compute, and APIs with unpredictable traffic. It has real costs in cold start latency, vendor coupling, and debugging complexity.",[20,5907,5908],{},"The teams that use serverless well are the ones who choose it deliberately for workloads where it fits, not the ones who adopt it as a general compute strategy and discover its limits in production.",[27,5910],{},[20,5912,5913,5914],{},"If you're evaluating whether serverless fits your system's architecture or need help with a specific implementation, ",[171,5915,5917],{"href":173,"rel":5916},[175],"let's connect.",[27,5919],{},[15,5921,183],{"id":182},[185,5923,5924,5930,5936,5942],{},[188,5925,5926],{},[171,5927,5929],{"href":5928},"/blog/architecture-decision-records","Architecture Decision Records: Why You Need Them and How to Write Them",[188,5931,5932],{},[171,5933,5935],{"href":5934},"/blog/clean-architecture-guide","Clean Architecture in Practice (Beyond the Circles Diagram)",[188,5937,5938],{},[171,5939,5941],{"href":5940},"/blog/event-driven-architecture-guide","Event-Driven Architecture: When It's the Right Call",[188,5943,5944],{},[171,5945,5947],{"href":5946},"/blog/hexagonal-architecture-guide","Hexagonal Architecture: Ports, Adapters, and the Core That Never Changes",{"title":213,"searchDepth":214,"depth":214,"links":5949},[5950,5951,5952,5958,5965,5966,5967,5968],{"id":5714,"depth":217,"text":5715},{"id":5729,"depth":217,"text":5730},{"id":5753,"depth":217,"text":5754,"children":5953},[5954,5955,5956,5957],{"id":5758,"depth":214,"text":5759},{"id":5768,"depth":214,"text":5769},{"id":5778,"depth":214,"text":5779},{"id":5785,"depth":214,"text":5786},{"id":5794,"depth":217,"text":5795,"children":5959},[5960,5961,5962,5963,5964],{"id":5798,"depth":214,"text":5799},{"id":5811,"depth":214,"text":5812},{"id":5821,"depth":214,"text":5822},{"id":5828,"depth":214,"text":5829},{"id":5835,"depth":214,"text":5836},{"id":5844,"depth":217,"text":5845},{"id":5862,"depth":217,"text":5863},{"id":5901,"depth":217,"text":5902},{"id":182,"depth":217,"text":183},"Serverless architecture offers compelling cost and scaling benefits — but it also introduces cold starts, vendor lock-in, and operational challenges. Here's when it's the right call and when it isn't.",[5971,5972,5973,5974,5975],"serverless architecture","serverless functions","AWS Lambda architecture","when to use serverless","serverless vs containers",{},"/blog/serverless-architecture-guide",{"title":5708,"description":5969},"blog/serverless-architecture-guide",[5981,5982,5983,5984],"Serverless Architecture","Cloud Computing","AWS Lambda","Cloudflare Workers","5pfezZ0xi3JI8ReOgjvXfhx3uvLokkFk5nCa5E2-xps",{"id":5987,"title":5988,"author":5989,"body":5990,"category":884,"date":224,"description":6280,"extension":226,"featured":227,"image":228,"keywords":6281,"meta":6287,"navigation":233,"path":6288,"readTime":877,"seo":6289,"stem":6290,"tags":6291,"__hash__":6296},"blog/blog/software-architect-skills.md","The Skills That Separate Software Architects from Senior Developers",{"name":9,"bio":10},{"type":12,"value":5991,"toc":6268},[5992,5996,5999,6002,6005,6007,6011,6014,6017,6043,6046,6052,6054,6058,6066,6069,6072,6077,6079,6083,6086,6089,6092,6098,6104,6110,6115,6117,6121,6124,6127,6130,6135,6137,6141,6144,6147,6150,6155,6157,6161,6164,6167,6173,6179,6185,6191,6196,6198,6202,6205,6208,6211,6214,6219,6221,6225,6228,6231,6238,6240,6242],[15,5993,5995],{"id":5994},"the-career-transition-nobody-explains-well","The Career Transition Nobody Explains Well",[20,5997,5998],{},"The path from senior developer to software architect is one of the most misunderstood transitions in tech. Most developers understand what makes a senior developer good: deep technical skill in their stack, the ability to solve complex problems, the judgment to write code that doesn't need rewriting in six months.",[20,6000,6001],{},"What makes a software architect good is harder to articulate because the job is partly different in kind, not just in degree. You're still deeply technical. You're still in the code. But the decisions you're making, the timescale you're reasoning across, and the skills that differentiate good from mediocre work have shifted.",[20,6003,6004],{},"This post is my attempt to articulate those skills precisely — not as a hiring rubric, but as a development map for developers who want to grow into architectural roles.",[27,6006],{},[15,6008,6010],{"id":6009},"skill-1-reasoning-about-constraints-not-just-solutions","Skill 1: Reasoning About Constraints, Not Just Solutions",[20,6012,6013],{},"Senior developers answer the question \"how do I implement this?\" Software architects answer the question \"given these constraints, what should we implement?\"",[20,6015,6016],{},"The constraints that matter in architectural work are not just technical. They include:",[185,6018,6019,6025,6031,6037],{},[188,6020,6021,6024],{},[36,6022,6023],{},"Team constraints",": How many engineers? What are their skill levels? How much cognitive load can the architecture add before it slows them down?",[188,6026,6027,6030],{},[36,6028,6029],{},"Time constraints",": When does something need to be in production? What's the cost of delaying three months to do it properly?",[188,6032,6033,6036],{},[36,6034,6035],{},"Operational constraints",": Who will operate this system? What's the on-call burden? What does the monitoring story look like?",[188,6038,6039,6042],{},[36,6040,6041],{},"Business constraints",": What's the regulatory environment? What are the data residency requirements? What integrations are non-negotiable?",[20,6044,6045],{},"Good software architects make these constraints explicit, reason about them systematically, and make trade-offs that are documented and defensible. \"We chose a monolith over microservices because the team is six people and we don't have the operational maturity to manage service topology\" is a good architectural decision. \"We built microservices because that's the modern approach\" is not.",[20,6047,6048,6051],{},[36,6049,6050],{},"How to develop this skill:"," Practice making the implicit explicit. When you make any significant technical decision, write down the constraints you're operating under and the trade-offs you're making. Review those records over time. You'll start to see patterns in which constraints you systematically underestimate.",[27,6053],{},[15,6055,6057],{"id":6056},"skill-2-cross-domain-technical-literacy","Skill 2: Cross-Domain Technical Literacy",[20,6059,6060,6061,6065],{},"Software architects don't need to be experts in everything. They need to be ",[6062,6063,6064],"em",{},"competent"," across a wide surface: databases, networking, security, frontend, backend, infrastructure, cloud services, data engineering. Competent means: able to have a substantive technical conversation, able to evaluate trade-offs, able to recognize when a problem is in a specific domain, and able to know what good looks like.",[20,6067,6068],{},"The architectural decisions that require this breadth: choosing where to put business logic (database triggers, application layer, event handlers), understanding the performance implications of joining across large tables vs. Denormalizing vs. Caching, knowing which security decisions have to be made at the data layer vs. The API layer vs. The network layer.",[20,6070,6071],{},"A software architect who is deep in backend but has no mental model of database performance will make backend decisions that cause database problems. One who doesn't understand frontend rendering will design APIs that create unnecessary performance problems on the client. The gaps show up in the decisions.",[20,6073,6074,6076],{},[36,6075,6050],{}," Pick one area outside your core expertise every six months and get to competency. Not mastery — competency. Enough to read a performance profile and know which questions to ask. Enough to recognize a naive implementation from a battle-tested one. Books, projects, and spending time with specialists in that domain are all effective.",[27,6078],{},[15,6080,6082],{"id":6081},"skill-3-making-decisions-under-uncertainty","Skill 3: Making Decisions Under Uncertainty",[20,6084,6085],{},"Software architecture involves making important decisions with incomplete information. You don't know exactly how the system will be used six months from now. You don't know whether the business will pivot. You don't know which of the three competing approaches will prove most maintainable as the codebase ages.",[20,6087,6088],{},"The naive response to uncertainty is paralysis — waiting for more information before deciding. This is usually the wrong move. The better response is: make the decision that preserves the most future options, document the uncertainty that drove it, and establish a process for revisiting it when more information arrives.",[20,6090,6091],{},"Specific techniques that help:",[20,6093,6094,6097],{},[36,6095,6096],{},"Distinguish reversible from irreversible decisions."," Irreversible decisions (database technology choice, primary API style, deployment model) deserve more analysis. Reversible decisions (specific library choice, naming conventions, file organization) can be made quickly and corrected later. Don't give equal weight to both categories.",[20,6099,6100,6103],{},[36,6101,6102],{},"Make assumptions explicit."," Every architectural decision rests on assumptions about usage patterns, team capabilities, business direction, or technology stability. Writing these assumptions down creates a basis for revisiting decisions when assumptions prove wrong.",[20,6105,6106,6109],{},[36,6107,6108],{},"Bias toward isolation."," When you're uncertain, build in a way that isolates the uncertain thing. A well-abstracted interface between two components means you can swap one component out when your assumptions prove wrong. A tightly coupled system means you're stuck.",[20,6111,6112,6114],{},[36,6113,6050],{}," Keep an architectural decision log. Every significant decision, the reasoning behind it, and the assumptions it rests on. Review it quarterly. Where were you right? Where were you wrong, and what did you miss?",[27,6116],{},[15,6118,6120],{"id":6119},"skill-4-communicating-technical-decisions-to-non-technical-stakeholders","Skill 4: Communicating Technical Decisions to Non-Technical Stakeholders",[20,6122,6123],{},"The software architect is often the person in the room who both understands the technical implications of a decision and has to secure resources (budget, time, people) to implement it. This requires being able to explain technical reasoning in terms that resonate with non-technical decision-makers.",[20,6125,6126],{},"This is not about dumbing things down. It's about translating between frames of reference. A business stakeholder doesn't care about database normalization. They care about whether their data will be correct, whether reporting will be fast, and whether the database will need to be rebuilt when the business grows. An architect who can connect the technical decision to those business outcomes can justify the investment.",[20,6128,6129],{},"The common failure mode: technical justifications that are correct but irrelevant to the audience. \"We should use PostgreSQL instead of MongoDB because the relational model better fits the domain\" is technically sound and will be greeted with blank stares by a CFO. \"PostgreSQL is the right choice here because our data has relationships that MongoDB would require us to manage manually, which introduces the risk of inconsistency in our financial records\" is the same argument translated into terms that matter to the audience.",[20,6131,6132,6134],{},[36,6133,6050],{}," Practice writing technical justifications in two versions: one for engineers, one for business stakeholders. Review the business-stakeholder version with someone who isn't technical and ask them what questions they still have. Answer those questions in the document. Repeat.",[27,6136],{},[15,6138,6140],{"id":6139},"skill-5-reading-a-system-that-already-exists","Skill 5: Reading a System That Already Exists",[20,6142,6143],{},"Most architectural work is not greenfield. It's working with systems that have history — design decisions made under different constraints, accumulated technical debt, components that made sense at the time and no longer do. Reading an existing system accurately is a distinct skill from designing a new one.",[20,6145,6146],{},"What accurate reading looks like: understanding not just what the system does but why it was built that way, distinguishing intentional constraints from accidental ones, recognizing where debt is load-bearing (changing it would break things) versus where it's inert (changing it is safe), and identifying which problems are systemic and which are local.",[20,6148,6149],{},"The skill that underlies this: the ability to hold a mental model of a system while updating it as new evidence arrives. Effective system reading is like detective work — you form hypotheses about how things work, test them by reading code and observing behavior, and update the model when the evidence contradicts the hypothesis.",[20,6151,6152,6154],{},[36,6153,6050],{}," Spend time with unfamiliar codebases deliberately. Open-source projects are good for this — they have complexity and history, and you can read them at your own pace. For each system, practice writing a brief architecture summary: what are the core components, how do they communicate, what's the data model, what are the non-obvious constraints? Compare your summary to any documentation that exists and see where you were wrong.",[27,6156],{},[15,6158,6160],{"id":6159},"skill-6-designing-for-operability-not-just-correctness","Skill 6: Designing for Operability, Not Just Correctness",[20,6162,6163],{},"A system can be architecturally correct and operationally miserable. Good software architect skills include thinking about what happens when the system is running in production: how do you know it's healthy? How do you diagnose problems when they occur? How do you roll out changes safely?",[20,6165,6166],{},"Operability decisions that have to be made at the architecture level:",[20,6168,6169,6172],{},[36,6170,6171],{},"Observability."," Structured logging, metrics, distributed tracing — these need to be designed in, not added after the fact. The places where logging matters most are usually not obvious until you're debugging a production incident at 2 AM.",[20,6174,6175,6178],{},[36,6176,6177],{},"Graceful degradation."," What does the system do when a dependency is unavailable? A system that fails hard when the payment processor is down is different from one that queues payment attempts for retry. The right behavior depends on the business context, but the behavior needs to be designed.",[20,6180,6181,6184],{},[36,6182,6183],{},"Deployment safety."," How do new versions of the system get deployed? Zero-downtime deployments, feature flags, canary releases — these are architectural concerns, not DevOps concerns added at the end.",[20,6186,6187,6190],{},[36,6188,6189],{},"Runbook design."," What are the likely failure modes, and is there a documented process for responding to each? Writing the runbook during system design, rather than during the first incident, reveals the missing observability and missing operational affordances.",[20,6192,6193,6195],{},[36,6194,6050],{}," Spend time on-call for systems you've helped build or are responsible for. Nothing teaches operability thinking faster than being woken up by a production alert and having to diagnose a running system in the dark. Retrospect on every incident: what observability was missing? What would have made the diagnosis faster?",[27,6197],{},[15,6199,6201],{"id":6200},"skill-7-knowing-when-not-to-architect","Skill 7: Knowing When Not to Architect",[20,6203,6204],{},"The most underrated software architect skill: knowing when the architecture is already good enough and adding more will make it worse.",[20,6206,6207],{},"The temptation toward over-architecture is real. Event-driven systems, microservices, CQRS, event sourcing — these are powerful patterns that solve real problems. They're also expensive: in implementation complexity, in operational overhead, and in developer cognitive load. Applied to problems they're not suited for, they create systems that are harder to work with than the alternatives they replaced.",[20,6209,6210],{},"The right question is not \"what is the most architecturally sophisticated approach?\" but \"what is the simplest architecture that meets the actual requirements?\"",[20,6212,6213],{},"A well-structured monolith is usually the right starting architecture for a new product. Microservices make sense when specific services need to scale independently and the team has the operational maturity to manage service topology. CQRS makes sense when read and write patterns are genuinely different enough that a unified model is creating real problems. These patterns solve problems; they shouldn't be added before the problems exist.",[20,6215,6216,6218],{},[36,6217,6050],{}," When you're drawn to a complex pattern, write down the specific problems it would solve. If you can't articulate at least two concrete problems it would solve in this system right now, you're over-architecting. Resist.",[27,6220],{},[15,6222,6224],{"id":6223},"the-common-thread","The Common Thread",[20,6226,6227],{},"Looking across these skills, the common thread is a kind of principled pragmatism: making decisions that are technically sound, business-aware, human-friendly, and appropriately humble about uncertainty. Good software architects are confident enough to make hard calls but honest enough to track their assumptions and update when they're wrong.",[20,6229,6230],{},"These skills develop through practice, reflection, and exposure to systems that have already made their mistakes. The fastest path is to work with experienced architects on real systems and pay attention — not just to what decisions are made, but to why, and to how those decisions look a year later.",[20,6232,6233,6234],{},"If you're looking for a software architect for a specific project or engagement, that's the work I do. ",[171,6235,6237],{"href":173,"rel":6236},[175],"Let's talk about what your project needs.",[27,6239],{},[15,6241,183],{"id":182},[185,6243,6244,6250,6256,6262],{},[188,6245,6246],{},[171,6247,6249],{"href":6248},"/blog/what-is-a-software-architect","What Is a Software Architect? (And Why Your Business Needs One)",[188,6251,6252],{},[171,6253,6255],{"href":6254},"/blog/software-architect-vs-software-engineer","Software Architect vs Software Engineer: What's Actually Different",[188,6257,6258],{},[171,6259,6261],{"href":6260},"/blog/enterprise-software-development-best-practices","Enterprise Software Best Practices (From Someone Who's Shipped It)",[188,6263,6264],{},[171,6265,6267],{"href":6266},"/blog/how-to-become-a-software-architect","How to Become a Software Architect (A Practitioner's Path)",{"title":213,"searchDepth":214,"depth":214,"links":6269},[6270,6271,6272,6273,6274,6275,6276,6277,6278,6279],{"id":5994,"depth":217,"text":5995},{"id":6009,"depth":217,"text":6010},{"id":6056,"depth":217,"text":6057},{"id":6081,"depth":217,"text":6082},{"id":6119,"depth":217,"text":6120},{"id":6139,"depth":217,"text":6140},{"id":6159,"depth":217,"text":6160},{"id":6200,"depth":217,"text":6201},{"id":6223,"depth":217,"text":6224},{"id":182,"depth":217,"text":183},"The jump from senior developer to software architect is not about knowing more languages — it's a shift in what you're optimizing for. Here are the specific skills that define effective software architects and how they actually develop them.",[6282,6283,6284,6285,6286],"software architect skills","what does a software architect do","software architecture","enterprise software development","hire software architect",{},"/blog/software-architect-skills",{"title":5988,"description":6280},"blog/software-architect-skills",[6292,6293,881,6294,6295],"Software Architecture","Career","Systems Design","Skills","rs4Y5IQue9Fr4PPMgtA4zgMGizQt1dRxCJkt5KnqoWw",{"id":6298,"title":6255,"author":6299,"body":6300,"category":884,"date":224,"description":6553,"extension":226,"featured":227,"image":228,"keywords":6554,"meta":6559,"navigation":233,"path":6254,"readTime":1232,"seo":6560,"stem":6561,"tags":6562,"__hash__":6563},"blog/blog/software-architect-vs-software-engineer.md",{"name":9,"bio":10},{"type":12,"value":6301,"toc":6535},[6302,6306,6309,6312,6314,6318,6324,6327,6334,6337,6339,6343,6347,6370,6374,6397,6404,6406,6410,6413,6416,6420,6423,6426,6428,6432,6436,6450,6454,6474,6477,6479,6483,6486,6489,6492,6494,6498,6501,6504,6506,6513,6515,6517],[15,6303,6305],{"id":6304},"the-question-that-always-comes-up","The Question That Always Comes Up",[20,6307,6308],{},"At some point, most engineers look at a job posting for \"Software Architect\" and wonder: what exactly is the difference between that and a very good software engineer? Both write code. Both review designs. Both care about whether the system works. The distinction isn't as obvious as the titles suggest, and in many organizations, it's deliberately blurry.",[20,6310,6311],{},"I've held both roles. Here's what I've learned about where the boundary actually sits — and why it matters when you're building a team or planning your own career.",[27,6313],{},[15,6315,6317],{"id":6316},"the-scope-of-responsibility","The Scope of Responsibility",[20,6319,6320,6321,177],{},"The clearest dividing line between an architect and an engineer is the ",[36,6322,6323],{},"scope of what they're accountable for",[20,6325,6326],{},"A software engineer owns a component, a service, a feature, or a codebase. Their job is to build things correctly — to translate requirements into working software, handle edge cases, write tests, and ship. A good engineer cares deeply about the quality of their module and understands the interfaces it exposes to the rest of the system.",[20,6328,6329,6330,6333],{},"A software architect owns the ",[36,6331,6332],{},"structure of the whole system"," — not the individual components, but the decisions that govern how those components relate to each other, how data flows between them, how the system fails, how it scales, and how it evolves over three to five years. Their scope extends beyond any single codebase to the organization building it.",[20,6335,6336],{},"This isn't about seniority. It's about orientation. Engineers look at the forest by examining the trees. Architects look at the forest first and ask whether they're even planting the right kind of trees.",[27,6338],{},[15,6340,6342],{"id":6341},"what-each-role-actually-does-day-to-day","What Each Role Actually Does Day-to-Day",[5756,6344,6346],{"id":6345},"what-software-engineers-do","What Software Engineers Do",[185,6348,6349,6352,6355,6358,6361,6364,6367],{},[188,6350,6351],{},"Implement features based on requirements or specs",[188,6353,6354],{},"Write unit and integration tests",[188,6356,6357],{},"Participate in code reviews",[188,6359,6360],{},"Debug production issues",[188,6362,6363],{},"Contribute to architecture discussions at the component level",[188,6365,6366],{},"Optimize specific algorithms, queries, or pipelines",[188,6368,6369],{},"Maintain and improve the codebase they own",[5756,6371,6373],{"id":6372},"what-software-architects-do","What Software Architects Do",[185,6375,6376,6379,6382,6385,6388,6391,6394],{},[188,6377,6378],{},"Define system boundaries: what services exist, how they communicate, where data lives",[188,6380,6381],{},"Evaluate technology choices with a bias toward long-term cost over short-term convenience",[188,6383,6384],{},"Write Architecture Decision Records (ADRs) to capture why decisions were made",[188,6386,6387],{},"Identify structural risks before they become production incidents",[188,6389,6390],{},"Bridge the gap between business requirements and technical capabilities — often translating in both directions",[188,6392,6393],{},"Design for failure: fault tolerance, graceful degradation, circuit breakers",[188,6395,6396],{},"Mentor senior engineers on system thinking and trade-off analysis",[20,6398,6399,6400,6403],{},"The architect's daily deliverable isn't code — it's ",[36,6401,6402],{},"clarity",". Clarity on constraints, clarity on trade-offs, clarity on what the system is allowed to become.",[27,6405],{},[15,6407,6409],{"id":6408},"where-the-confusion-comes-from","Where the Confusion Comes From",[20,6411,6412],{},"In small companies and startups, one person often does both jobs. A principal engineer or CTO might be doing architecture without the title, because there aren't enough people to specialize. That's fine, but it creates a long-term confusion: \"our senior engineers already think architecturally, why do we need an architect?\"",[20,6414,6415],{},"The answer surfaces around the 20-50 engineer mark, when systems have grown complex enough that no single person holds the full picture anymore. That's when the absence of architectural governance becomes a scaling liability — and companies that conflated the roles start accumulating coordination debt.",[5756,6417,6419],{"id":6418},"the-skill-overlap-is-real","The Skill Overlap Is Real",[20,6421,6422],{},"Both roles require deep technical knowledge. An architect who can't read a pull request and understand its systemic implications isn't doing their job. An engineer who can't reason about the system beyond their service boundaries will hit a ceiling. The best engineers think architecturally; the best architects stay close enough to the code to have credibility.",[20,6424,6425],{},"But good code and good architecture are not the same thing. You can have beautifully written microservices that collectively form a disaster — wrong service boundaries, chatty synchronous calls where you needed async, a data model that makes business logic impossible to express cleanly. That's an architectural failure. It won't show up in a linting tool.",[27,6427],{},[15,6429,6431],{"id":6430},"when-you-need-an-architect-vs-an-engineer","When You Need an Architect vs an Engineer",[5756,6433,6435],{"id":6434},"hire-engineers-when","Hire engineers when:",[185,6437,6438,6441,6444,6447],{},[188,6439,6440],{},"You have well-defined features to build",[188,6442,6443],{},"Your architecture is stable and the main work is execution",[188,6445,6446],{},"You need velocity within an existing system",[188,6448,6449],{},"Your coordination overhead is manageable",[5756,6451,6453],{"id":6452},"bring-in-an-architect-when","Bring in an architect when:",[185,6455,6456,6459,6462,6465,6468,6471],{},[188,6457,6458],{},"You're starting a new product and need to make foundational decisions that will be expensive to undo",[188,6460,6461],{},"Your system is growing and teams are stepping on each other because service boundaries aren't clear",[188,6463,6464],{},"You're migrating from a legacy system and need a strategy — not just a timeline",[188,6466,6467],{},"Your team is shipping fast but accumulating technical debt that's beginning to compound",[188,6469,6470],{},"You're evaluating a platform shift: cloud migration, re-platforming, microservices adoption",[188,6472,6473],{},"Business requirements are changing faster than the system can adapt",[20,6475,6476],{},"The tell is usually this: when engineering teams are consistently blocked by design decisions that haven't been made, or when the answer to \"how does this work?\" varies depending on who you ask — that's an architecture problem, not an engineering problem.",[27,6478],{},[15,6480,6482],{"id":6481},"the-career-path-reality","The Career Path Reality",[20,6484,6485],{},"Most architects get there through engineering. You spend years writing code, you develop a sense for what decisions matter at scale, you start getting pulled into design conversations, and eventually your value shifts from \"can implement this feature\" to \"can identify why this approach will fail in 18 months.\"",[20,6487,6488],{},"The transition isn't automatic. Many excellent engineers never develop the system-thinking and communication skills that architecture demands. Architecture requires you to hold multiple views of a system simultaneously — the current state, the desired state, the migration path — and communicate trade-offs to people who have different levels of technical depth.",[20,6490,6491],{},"It also requires a tolerance for ambiguity. Engineers often work toward a clear definition of done. Architects work in environments where the requirements are incomplete, the constraints are shifting, and the right answer depends on business priorities that might change next quarter.",[27,6493],{},[15,6495,6497],{"id":6496},"one-thing-they-have-in-common","One Thing They Have in Common",[20,6499,6500],{},"The best engineers and the best architects share one quality: they think about the person who inherits their work. The engineer thinks about the next developer who has to debug this function. The architect thinks about the next team that has to extend this system.",[20,6502,6503],{},"That perspective — building for the future without knowing exactly what the future looks like — is the most important skill in either role.",[27,6505],{},[20,6507,6508,6509],{},"If you're thinking through your team structure or evaluating a technical leadership hire, I'm happy to have a direct conversation about what you actually need. ",[171,6510,6512],{"href":173,"rel":6511},[175],"Schedule a call here.",[27,6514],{},[15,6516,183],{"id":182},[185,6518,6519,6523,6527,6531],{},[188,6520,6521],{},[171,6522,6267],{"href":6266},[188,6524,6525],{},[171,6526,6249],{"href":6248},[188,6528,6529],{},[171,6530,5988],{"href":6288},[188,6532,6533],{},[171,6534,6261],{"href":6260},{"title":213,"searchDepth":214,"depth":214,"links":6536},[6537,6538,6539,6543,6546,6550,6551,6552],{"id":6304,"depth":217,"text":6305},{"id":6316,"depth":217,"text":6317},{"id":6341,"depth":217,"text":6342,"children":6540},[6541,6542],{"id":6345,"depth":214,"text":6346},{"id":6372,"depth":214,"text":6373},{"id":6408,"depth":217,"text":6409,"children":6544},[6545],{"id":6418,"depth":214,"text":6419},{"id":6430,"depth":217,"text":6431,"children":6547},[6548,6549],{"id":6434,"depth":214,"text":6435},{"id":6452,"depth":214,"text":6453},{"id":6481,"depth":217,"text":6482},{"id":6496,"depth":217,"text":6497},{"id":182,"depth":217,"text":183},"Software architect vs software engineer is more than a title difference — the scope, mindset, and accountability are fundamentally different. Here's the honest breakdown.",[6555,6556,6557,6558],"software architect vs software engineer","software architect role","software engineer career","when to hire a software architect",{},{"title":6255,"description":6553},"blog/software-architect-vs-software-engineer",[6292,6293,545,6294],"QpA6NaC2USGCKQuloq5rDNN7su6dut2RiDv5wqODES0",{"id":6565,"title":6566,"author":6567,"body":6568,"category":884,"date":224,"description":6885,"extension":226,"featured":227,"image":228,"keywords":6886,"meta":6892,"navigation":233,"path":6893,"readTime":1351,"seo":6894,"stem":6895,"tags":6896,"__hash__":6899},"blog/blog/software-architecture-patterns.md","Software Architecture Patterns Every Architect Should Know",{"name":9,"bio":10},{"type":12,"value":6569,"toc":6855},[6570,6574,6577,6580,6582,6586,6590,6593,6597,6600,6603,6607,6610,6616,6622,6624,6628,6631,6634,6637,6640,6643,6646,6666,6671,6677,6679,6683,6686,6689,6692,6695,6698,6701,6706,6711,6713,6717,6720,6723,6726,6729,6732,6735,6740,6745,6747,6751,6754,6757,6760,6763,6766,6769,6774,6779,6781,6785,6788,6814,6817,6820,6822,6829,6831,6833],[15,6571,6573],{"id":6572},"pattern-knowledge-is-only-useful-with-judgment","Pattern Knowledge Is Only Useful With Judgment",[20,6575,6576],{},"Architecture patterns are frequently taught as if knowing them is the goal. It isn't. The goal is knowing when to apply each one — and equally important, when not to. Every pattern in this list solves a real problem. Every pattern in this list has also been misapplied in production systems I've had to untangle.",[20,6578,6579],{},"What follows is a practical breakdown of the patterns I reach for most often, including the context where they make sense and the warning signs that you're applying them incorrectly.",[27,6581],{},[15,6583,6585],{"id":6584},"layered-architecture","Layered Architecture",[5756,6587,6589],{"id":6588},"what-it-is","What It Is",[20,6591,6592],{},"The layered pattern (also called N-tier) divides a system into horizontal layers where each layer only communicates with the layer immediately below it. The classic breakdown: Presentation → Application → Domain → Infrastructure.",[5756,6594,6596],{"id":6595},"why-it-works","Why It Works",[20,6598,6599],{},"Layered architecture provides clear separation of concerns. Your business logic doesn't know anything about your database. Your API controllers don't contain business rules. Each layer has a defined responsibility and a defined boundary.",[20,6601,6602],{},"For most applications, this is the right starting point. It's well-understood, straightforward to implement, and easy to test. Most modern frameworks enforce some version of it.",[5756,6604,6606],{"id":6605},"when-it-falls-apart","When It Falls Apart",[20,6608,6609],{},"Layered architectures have a tendency to develop \"fat\" middle layers, particularly the application/service layer, which becomes a dumping ground for business logic that doesn't have an obvious home. They can also encourage \"lasagna code\" — so many thin, indirection-heavy layers that simple operations require traversing the entire stack.",[20,6611,6612,6615],{},[36,6613,6614],{},"Use it when:"," You're building a standard web application with CRUD operations and moderate business complexity. This covers a lot of real-world software.",[20,6617,6618,6621],{},[36,6619,6620],{},"Reconsider when:"," Your domain logic is genuinely complex, your system needs to support multiple interfaces (API, event-driven, CLI), or you're building something that will evolve significantly over time.",[27,6623],{},[15,6625,6627],{"id":6626},"microservices-architecture","Microservices Architecture",[5756,6629,6589],{"id":6630},"what-it-is-1",[20,6632,6633],{},"Microservices decomposes a system into independently deployable services, each owning its own data and being responsible for a specific business capability. Services communicate via APIs or messaging.",[5756,6635,6596],{"id":6636},"why-it-works-1",[20,6638,6639],{},"Done well, microservices enable independent scaling, independent deployment, and organizational alignment — each team owns one or a few services and can ship without coordinating with every other team.",[5756,6641,6606],{"id":6642},"when-it-falls-apart-1",[20,6644,6645],{},"Microservices are one of the most misapplied patterns in modern software. The problems I see most often:",[185,6647,6648,6654,6660],{},[188,6649,6650,6653],{},[36,6651,6652],{},"Distributed monolith:"," Services are fine-grained at the technical level but tightly coupled at the business level. Deploying Service A requires deploying Service B and C simultaneously. You've taken on all the costs of microservices with none of the independence.",[188,6655,6656,6659],{},[36,6657,6658],{},"Wrong service boundaries:"," Services carved by technical function (UserService, DatabaseService) instead of business capability. These create constant cross-service coordination for real features.",[188,6661,6662,6665],{},[36,6663,6664],{},"Premature adoption:"," A team of 5 engineers building a startup adopts microservices because Netflix uses them. Netflix has thousands of engineers and multiple years of organic growth that led to that architecture organically.",[20,6667,6668,6670],{},[36,6669,6614],{}," You have distinct, bounded business domains, teams large enough to own services independently, and operational maturity to handle distributed systems complexity. The system is already large and growing.",[20,6672,6673,6676],{},[36,6674,6675],{},"Avoid it when:"," You're early-stage, your team is small, or your domain boundaries aren't yet clear. Start with a well-structured monolith.",[27,6678],{},[15,6680,6682],{"id":6681},"event-driven-architecture","Event-Driven Architecture",[5756,6684,6589],{"id":6685},"what-it-is-2",[20,6687,6688],{},"Services communicate by publishing and subscribing to events rather than calling each other directly. A service emits an event when something noteworthy happens; other services react to those events asynchronously.",[5756,6690,6596],{"id":6691},"why-it-works-2",[20,6693,6694],{},"Event-driven systems achieve loose coupling. The publisher doesn't know or care who's listening. Adding a new downstream consumer requires no changes to the upstream service. This is powerful for systems that need to evolve independently and scale different components at different rates.",[5756,6696,6606],{"id":6697},"when-it-falls-apart-2",[20,6699,6700],{},"Event-driven systems introduce significant complexity: eventual consistency, event ordering, duplicate delivery, schema evolution of event contracts, and distributed tracing across async flows. Debugging a production issue that spans five event consumers is genuinely hard.",[20,6702,6703,6705],{},[36,6704,6614],{}," You have genuinely asynchronous workflows, you need to decouple producers from consumers, or you need to support fan-out (one event, multiple consumers).",[20,6707,6708,6710],{},[36,6709,6675],{}," You need immediate consistency, the workflow is inherently synchronous, or your team isn't equipped for the operational complexity.",[27,6712],{},[15,6714,6716],{"id":6715},"hexagonal-architecture-ports-and-adapters","Hexagonal Architecture (Ports and Adapters)",[5756,6718,6589],{"id":6719},"what-it-is-3",[20,6721,6722],{},"Hexagonal architecture puts your domain logic at the center, surrounded by \"ports\" (interfaces your domain exposes or depends on) and \"adapters\" (implementations that connect your domain to the outside world: databases, APIs, UIs, message queues).",[5756,6724,6596],{"id":6725},"why-it-works-3",[20,6727,6728],{},"Your domain logic becomes truly independent of infrastructure. You can swap out the database without touching business rules. You can test business logic in complete isolation from the network. The same domain core can serve a REST API, a GraphQL API, and a message consumer simultaneously.",[5756,6730,6606],{"id":6731},"when-it-falls-apart-3",[20,6733,6734],{},"The pattern adds boilerplate. For every external dependency, you're writing an interface plus an implementation. For simple CRUD systems, this overhead rarely pays off. It's also commonly misunderstood — I've seen teams create \"adapters\" that are just thin wrappers around ORMs, with the actual data mapping logic bleeding into the domain layer anyway.",[20,6736,6737,6739],{},[36,6738,6614],{}," Your domain logic is complex and you want to test it independently. When you're building something long-lived that will outlast your current infrastructure choices.",[20,6741,6742,6744],{},[36,6743,6675],{}," Your application is primarily data access with thin business logic. The pattern creates overhead that won't pay off.",[27,6746],{},[15,6748,6750],{"id":6749},"cqrs-command-query-responsibility-segregation","CQRS (Command Query Responsibility Segregation)",[5756,6752,6589],{"id":6753},"what-it-is-4",[20,6755,6756],{},"CQRS separates the read path from the write path. Commands change state. Queries read state. These can be handled by different models, different services, even different databases.",[5756,6758,6596],{"id":6759},"why-it-works-4",[20,6761,6762],{},"Complex domains often have asymmetric read and write requirements. You might write data through a rich domain model with complex validation and business rules, but read it through flat, denormalized projections optimized for display. Combining these in a single model creates constant tension. CQRS eliminates that tension by making the separation explicit.",[5756,6764,6606],{"id":6765},"when-it-falls-apart-4",[20,6767,6768],{},"CQRS significantly increases architectural complexity. You now have two models to maintain, potentially two data stores to keep in sync, and eventual consistency between them. For most applications, this complexity is not justified.",[20,6770,6771,6773],{},[36,6772,6614],{}," You have a domain with complex business rules on the write side and diverse, performance-sensitive read requirements. Often paired with Event Sourcing.",[20,6775,6776,6778],{},[36,6777,6675],{}," Your read and write requirements are symmetric, your domain is simple, or your team isn't equipped to manage the operational overhead.",[27,6780],{},[15,6782,6784],{"id":6783},"choosing-the-right-pattern","Choosing the Right Pattern",[20,6786,6787],{},"No architecture pattern is universally correct. The decision depends on:",[185,6789,6790,6796,6802,6808],{},[188,6791,6792,6795],{},[36,6793,6794],{},"Team size and structure:"," Small teams can't afford the overhead of microservices or CQRS. Large, federated teams can't coordinate around a monolith.",[188,6797,6798,6801],{},[36,6799,6800],{},"Domain complexity:"," Complex domains justify sophisticated patterns. Simple domains don't.",[188,6803,6804,6807],{},[36,6805,6806],{},"Operational maturity:"," Distributed systems require sophisticated observability, deployment pipelines, and incident response. If you don't have these, adopt simpler patterns until you do.",[188,6809,6810,6813],{},[36,6811,6812],{},"Stage of the business:"," Early-stage products need to move fast and pivot. Heavyweight patterns add friction. Mature products with known domains can afford to invest in structural clarity.",[20,6815,6816],{},"The pattern I reach for most often for new systems is a well-structured modular monolith with hexagonal architecture inside. It provides the separation of concerns and testability of the more complex patterns without the operational overhead. If the system outgrows it, the modular boundaries make it straightforward to extract services.",[20,6818,6819],{},"Start simple. Add complexity when the problem demands it, not when the pattern looks interesting.",[27,6821],{},[20,6823,6824,6825],{},"If you're evaluating which architectural pattern fits your current system — or trying to untangle one that's grown beyond its pattern — ",[171,6826,6828],{"href":173,"rel":6827},[175],"let's talk.",[27,6830],{},[15,6832,183],{"id":182},[185,6834,6835,6841,6847,6851],{},[188,6836,6837],{},[171,6838,6840],{"href":6839},"/blog/design-patterns-for-architects","Software Design Patterns Every Architect Should Have in Their Toolkit",[188,6842,6843],{},[171,6844,6846],{"href":6845},"/blog/distributed-systems-fundamentals","Distributed Systems Fundamentals Every Developer Should Know",[188,6848,6849],{},[171,6850,6255],{"href":6254},[188,6852,6853],{},[171,6854,6249],{"href":6248},{"title":213,"searchDepth":214,"depth":214,"links":6856},[6857,6858,6863,6868,6873,6878,6883,6884],{"id":6572,"depth":217,"text":6573},{"id":6584,"depth":217,"text":6585,"children":6859},[6860,6861,6862],{"id":6588,"depth":214,"text":6589},{"id":6595,"depth":214,"text":6596},{"id":6605,"depth":214,"text":6606},{"id":6626,"depth":217,"text":6627,"children":6864},[6865,6866,6867],{"id":6630,"depth":214,"text":6589},{"id":6636,"depth":214,"text":6596},{"id":6642,"depth":214,"text":6606},{"id":6681,"depth":217,"text":6682,"children":6869},[6870,6871,6872],{"id":6685,"depth":214,"text":6589},{"id":6691,"depth":214,"text":6596},{"id":6697,"depth":214,"text":6606},{"id":6715,"depth":217,"text":6716,"children":6874},[6875,6876,6877],{"id":6719,"depth":214,"text":6589},{"id":6725,"depth":214,"text":6596},{"id":6731,"depth":214,"text":6606},{"id":6749,"depth":217,"text":6750,"children":6879},[6880,6881,6882],{"id":6753,"depth":214,"text":6589},{"id":6759,"depth":214,"text":6596},{"id":6765,"depth":214,"text":6606},{"id":6783,"depth":217,"text":6784},{"id":182,"depth":217,"text":183},"Software architecture patterns are the vocabulary of system design. This guide breaks down layered, microservices, event-driven, hexagonal, and CQRS — with honest guidance on when to use each.",[6887,6888,6889,6890,6891],"software architecture patterns","system design patterns","microservices architecture","event-driven architecture","hexagonal architecture",{},"/blog/software-architecture-patterns",{"title":6566,"description":6885},"blog/software-architecture-patterns",[6292,6897,6294,6898],"Design Patterns","Microservices","VS3PdPn_yhe3gHov4Un6a10eFwgE9Gg7vIMLb3_VBTc",{"id":6901,"title":6902,"author":6903,"body":6904,"category":884,"date":224,"description":7290,"extension":226,"featured":227,"image":228,"keywords":7291,"meta":7297,"navigation":233,"path":7298,"readTime":1232,"seo":7299,"stem":7300,"tags":7301,"__hash__":7305},"blog/blog/software-documentation-best-practices.md","Software Documentation That Engineers Actually Read",{"name":9,"bio":10},{"type":12,"value":6905,"toc":7277},[6906,6910,6913,6916,6919,6921,6925,6928,6934,6940,6946,6952,6955,6957,6961,6964,6967,6973,6979,6985,6991,6997,7003,7006,7010,7016,7029,7035,7037,7041,7044,7047,7050,7052,7056,7059,7062,7068,7090,7096,7102,7108,7110,7114,7117,7120,7123,7133,7139,7145,7151,7157,7159,7163,7166,7169,7183,7186,7203,7205,7209,7212,7218,7224,7230,7236,7238,7241,7244,7246,7253,7255,7257],[15,6907,6909],{"id":6908},"the-problem-with-most-documentation","The Problem With Most Documentation",[20,6911,6912],{},"Most software documentation has one of two problems: there's too little of it, or there's too much of it in the wrong places. Both are equally useless.",[20,6914,6915],{},"Too little documentation means engineers waste time reverse-engineering decisions that should have been written down, new team members take months to become productive, and tribal knowledge evaporates whenever someone leaves. Too much documentation — comprehensive wikis that nobody reads, auto-generated API docs with no examples, architectural diagrams that haven't been updated in two years — creates noise that buries the signal.",[20,6917,6918],{},"The goal isn't comprehensive documentation. The goal is useful documentation: the minimum viable set of documents that genuinely helps engineers understand the system, make good decisions, and solve problems without interrupting each other. Here's how to build it.",[27,6920],{},[15,6922,6924],{"id":6923},"the-documentation-hierarchy","The Documentation Hierarchy",[20,6926,6927],{},"Different documentation types serve different purposes. Understanding which type serves which purpose helps you write the right document instead of the most comprehensive one.",[20,6929,6930,6933],{},[36,6931,6932],{},"Level 1 — Orientating documentation:"," Helps new engineers understand what the system is and how to start working with it. READMEs, architecture overviews, \"getting started\" guides.",[20,6935,6936,6939],{},[36,6937,6938],{},"Level 2 — Decision documentation:"," Captures why things are the way they are. Architecture Decision Records, design documents, post-mortems.",[20,6941,6942,6945],{},[36,6943,6944],{},"Level 3 — Reference documentation:"," Describes the specifics of an interface or system. API docs, configuration references, data model documentation.",[20,6947,6948,6951],{},[36,6949,6950],{},"Level 4 — Operational documentation:"," Helps engineers operate the system in production. Runbooks, on-call guides, deployment procedures.",[20,6953,6954],{},"Each type has a different audience, a different level of detail, and a different maintenance burden. Write the right type for the job.",[27,6956],{},[15,6958,6960],{"id":6959},"readmes-that-actually-orient","READMEs That Actually Orient",[20,6962,6963],{},"A README is a contract with the engineer who just pulled your repository for the first time. It has thirty seconds to tell them what they need to know to get started, or they'll give up and ping Slack instead.",[20,6965,6966],{},"A useful README answers exactly these questions, in this order:",[20,6968,6969,6972],{},[36,6970,6971],{},"What is this?"," One or two sentences. Not marketing copy. What does this service do in concrete terms?",[20,6974,6975,6978],{},[36,6976,6977],{},"How do I run it locally?"," Step-by-step instructions assuming nothing. Include every dependency, every environment variable, every setup command. Test these instructions on a clean machine periodically — they drift.",[20,6980,6981,6984],{},[36,6982,6983],{},"What are the key concepts?"," If the system has domain-specific terms or non-obvious architectural concepts, explain them briefly. Link to deeper documentation.",[20,6986,6987,6990],{},[36,6988,6989],{},"How do I run the tests?"," The command, what it runs, and how to interpret the output.",[20,6992,6993,6996],{},[36,6994,6995],{},"How do I deploy?"," Or where to find deployment documentation if it's complex enough to warrant its own document.",[20,6998,6999,7002],{},[36,7000,7001],{},"Who owns this?"," Team name, Slack channel, on-call rotation. The README is often how someone figures out who to contact when things break.",[20,7004,7005],{},"That's it. Don't put architectural design in the README — that belongs in an ADR or design doc. Don't put API reference in the README — that belongs in API docs. The README exists to orient, and it should do that quickly.",[5756,7007,7009],{"id":7008},"what-kills-readmes","What Kills READMEs",[20,7011,7012,7015],{},[36,7013,7014],{},"Stale instructions."," A README that lies is worse than no README. If the setup instructions no longer work, the README has negative value — it wastes time and erodes trust. Either fix it or delete the section.",[20,7017,7018,7021,7022,7025,7026,7028],{},[36,7019,7020],{},"Assuming context."," \"Configure your environment variables\" is not setup documentation. \"Copy ",[287,7023,7024],{},".env.example"," to ",[287,7027,329],{}," and fill in the following values:\" is.",[20,7030,7031,7034],{},[36,7032,7033],{},"Everything in one file."," A 2,000-line README is not a README. It's a poorly organized wiki. Extract the depth into linked documents.",[27,7036],{},[15,7038,7040],{"id":7039},"architecture-decision-records-documentation-that-ages-well","Architecture Decision Records: Documentation That Ages Well",[20,7042,7043],{},"ADRs are covered in detail in another post, but they belong in the documentation discussion because they're the type of documentation most reliably ignored — and the most valuable when present.",[20,7045,7046],{},"An ADR captures why a significant decision was made. Not what the decision was (that's visible in the code), but the context, alternatives, and trade-offs. This ages extremely well because the context of a decision is exactly what gets lost over time.",[20,7048,7049],{},"The key practice: write the ADR as part of the decision-making process, not after. Bring the draft to the architecture review. Update it based on the discussion. Merge it with the code change it documents. Three sentences written while the decision is fresh are worth more than three paragraphs written six months later from memory.",[27,7051],{},[15,7053,7055],{"id":7054},"api-documentation-engineers-will-actually-use","API Documentation Engineers Will Actually Use",[20,7057,7058],{},"Auto-generated API documentation is a floor, not a ceiling. OpenAPI/Swagger specifications, generated from code annotations, provide accurate reference documentation automatically. This is necessary. It's not sufficient.",[20,7060,7061],{},"What generated docs don't provide:",[20,7063,7064,7067],{},[36,7065,7066],{},"Getting started guides."," How does a new integration developer make their first successful API call? Walk them through authentication, a simple request, and error handling in a single, end-to-end example. This can't be generated.",[20,7069,7070,7073,7074,7077,7078,7081,7082,7085,7086,7089],{},[36,7071,7072],{},"Conceptual explanations."," The difference between a ",[287,7075,7076],{},"draft"," and a ",[287,7079,7080],{},"published"," resource. When to use ",[287,7083,7084],{},"PATCH"," vs ",[287,7087,7088],{},"PUT",". What \"idempotency key\" means in your context. Generated docs can describe fields; they can't explain concepts.",[20,7091,7092,7095],{},[36,7093,7094],{},"Error code documentation."," Every machine-readable error code your API returns deserves a human-readable explanation and suggested remediation. \"Order cannot be placed while payment is pending\" is useful. \"ERR_422\" is not.",[20,7097,7098,7101],{},[36,7099,7100],{},"Realistic examples."," Generated examples often use placeholder values. Real examples with actual representative data — the kinds of payloads your API actually produces and consumes — reduce integration errors.",[20,7103,7104,7107],{},[36,7105,7106],{},"Change log."," What changed between API versions, and what should integrators do about it?",[27,7109],{},[15,7111,7113],{"id":7112},"runbooks-documentation-that-works-at-3am","Runbooks: Documentation That Works at 3am",[20,7115,7116],{},"A runbook is operational documentation for a specific service or system — the procedures an on-call engineer needs when something goes wrong. It's the documentation equivalent of a decision in advance.",[20,7118,7119],{},"A runbook that isn't clear enough to follow at 3am under incident stress is not a runbook. It's a document.",[20,7121,7122],{},"Good runbooks are:",[20,7124,7125,7128,7129,7132],{},[36,7126,7127],{},"Specific."," \"Check the service health\" is not a runbook step. \"Run ",[287,7130,7131],{},"kubectl get pods -n payment-service"," and verify all pods are in Running state\" is.",[20,7134,7135,7138],{},[36,7136,7137],{},"Linked to alerting."," When an alert fires, the runbook link should be in the alert. Engineers should never have to search for the runbook for a specific alert.",[20,7140,7141,7144],{},[36,7142,7143],{},"Actionable."," For each symptom, what do you check? What do you do? What's the escalation path if the standard remediation doesn't work?",[20,7146,7147,7150],{},[36,7148,7149],{},"Tested."," Runbooks that have never been followed in practice are full of errors. Run through them during game days or chaos engineering exercises to find the gaps before they become incident gaps.",[20,7152,7153,7156],{},[36,7154,7155],{},"Current."," Every architecture change that affects operations should trigger a runbook review. Runbooks that describe a system that no longer exists are dangerous.",[27,7158],{},[15,7160,7162],{"id":7161},"the-documentation-that-doesnt-need-to-exist","The Documentation That Doesn't Need to Exist",[20,7164,7165],{},"Not everything needs documentation. Being selective about what you document is as important as writing good documentation for the things that matter.",[20,7167,7168],{},"You don't need to document:",[185,7170,7171,7174,7177,7180],{},[188,7172,7173],{},"Code that is self-explanatory (well-named functions with clear logic)",[188,7175,7176],{},"Implementation details that are visible in the code",[188,7178,7179],{},"Decisions that are trivially reversible",[188,7181,7182],{},"Architecture diagrams for their own sake, with no reader in mind",[20,7184,7185],{},"You do need to document:",[185,7187,7188,7191,7194,7197,7200],{},[188,7189,7190],{},"The non-obvious reasons behind decisions",[188,7192,7193],{},"Anything a new engineer would need to know to get productive",[188,7195,7196],{},"Operational procedures for production systems",[188,7198,7199],{},"API contracts that other teams consume",[188,7201,7202],{},"Complex domain concepts that aren't obvious from the code",[27,7204],{},[15,7206,7208],{"id":7207},"making-documentation-sustainable","Making Documentation Sustainable",[20,7210,7211],{},"Documentation that nobody maintains is documentation that nobody trusts. Make documentation maintenance sustainable:",[20,7213,7214,7217],{},[36,7215,7216],{},"Co-locate documentation with code."," Docs that live next to the code they describe are updated when the code changes. Docs in a separate wiki are updated when someone remembers.",[20,7219,7220,7223],{},[36,7221,7222],{},"Make documentation part of the definition of done."," If a feature requires an API change, the ADR and API docs are part of that feature, not separate work.",[20,7225,7226,7229],{},[36,7227,7228],{},"Review documentation in code review."," If a PR changes behavior and the relevant runbook or README isn't updated, the PR isn't done.",[20,7231,7232,7235],{},[36,7233,7234],{},"Delete stale documentation ruthlessly."," Outdated documentation is worse than no documentation. A quarterly documentation audit that deletes more than it creates is a sign of a healthy documentation practice.",[27,7237],{},[20,7239,7240],{},"The benchmark for useful documentation is simple: does it help the people who need to use it? If your on-call engineer reaches for the runbook and finds what they need, the runbook is working. If your new hire reads the README and can run the service in an hour, the README is working. If nobody reads the wiki, the wiki isn't working.",[20,7242,7243],{},"Write for the reader. Write for the moment they need it most.",[27,7245],{},[20,7247,7248,7249],{},"If you're building out an engineering documentation practice or auditing your current state, ",[171,7250,7252],{"href":173,"rel":7251},[175],"I'm happy to consult.",[27,7254],{},[15,7256,183],{"id":182},[185,7258,7259,7263,7267,7273],{},[188,7260,7261],{},[171,7262,5929],{"href":5928},[188,7264,7265],{},[171,7266,6255],{"href":6254},[188,7268,7269],{},[171,7270,7272],{"href":7271},"/blog/developer-experience-improvements","Developer Experience: The Hidden Multiplier on Team Output",[188,7274,7275],{},[171,7276,6267],{"href":6266},{"title":213,"searchDepth":214,"depth":214,"links":7278},[7279,7280,7281,7284,7285,7286,7287,7288,7289],{"id":6908,"depth":217,"text":6909},{"id":6923,"depth":217,"text":6924},{"id":6959,"depth":217,"text":6960,"children":7282},[7283],{"id":7008,"depth":214,"text":7009},{"id":7039,"depth":217,"text":7040},{"id":7054,"depth":217,"text":7055},{"id":7112,"depth":217,"text":7113},{"id":7161,"depth":217,"text":7162},{"id":7207,"depth":217,"text":7208},{"id":182,"depth":217,"text":183},"Software documentation best practices focus on creating docs that serve a purpose, stay current, and get used. Here's what actually matters across READMEs, ADRs, API docs, and runbooks.",[7292,7293,7294,7295,7296],"software documentation best practices","engineering documentation","README best practices","API documentation","runbook documentation",{},"/blog/software-documentation-best-practices",{"title":6902,"description":7290},"blog/software-documentation-best-practices",[7302,6292,7303,7304],"Documentation","Engineering Culture","Developer Experience","OBzT0SGFJhsbD9oVlzDz7L5ySgDYV9S8PBodJvoYq48",{"id":7307,"title":7308,"author":7309,"body":7310,"category":884,"date":224,"description":7587,"extension":226,"featured":227,"image":228,"keywords":7588,"meta":7594,"navigation":233,"path":7595,"readTime":877,"seo":7596,"stem":7597,"tags":7598,"__hash__":7602},"blog/blog/software-estimation-techniques.md","Software Estimation: Why It's Hard and How to Do It Better",{"name":9,"bio":10},{"type":12,"value":7311,"toc":7565},[7312,7316,7319,7322,7329,7332,7334,7338,7342,7345,7348,7352,7355,7358,7362,7365,7369,7372,7374,7378,7381,7384,7387,7391,7394,7397,7399,7403,7406,7409,7423,7426,7429,7431,7435,7439,7442,7445,7449,7456,7460,7463,7467,7470,7472,7476,7482,7488,7494,7500,7502,7506,7509,7529,7532,7534,7541,7543,7545],[15,7313,7315],{"id":7314},"the-estimation-problem-is-not-what-most-people-think","The Estimation Problem Is Not What Most People Think",[20,7317,7318],{},"Every manager who has watched a software project run late has an opinion on why engineers can't estimate. The common theories: engineers are optimists who ignore risk, they don't account for meetings and interruptions, they underestimate complexity, they forget testing and code review.",[20,7320,7321],{},"All of these are partially true. None of them are the actual root cause.",[20,7323,7324,7325,7328],{},"The actual root cause: ",[36,7326,7327],{},"software estimation is inherently forecasting under uncertainty, and humans are systematically bad at forecasting under uncertainty — especially for novel, complex work."," This isn't a character flaw. It's a cognitive limitation that applies to engineers, managers, and every other professional who estimates novel complex work for a living.",[20,7330,7331],{},"Understanding why estimation is hard is the prerequisite for doing it better.",[27,7333],{},[15,7335,7337],{"id":7336},"why-software-estimates-are-wrong","Why Software Estimates Are Wrong",[5756,7339,7341],{"id":7340},"the-planning-fallacy","The Planning Fallacy",[20,7343,7344],{},"Nobel laureate Daniel Kahneman documented a systematic bias he called the planning fallacy: people consistently predict that their projects will proceed according to the best-case scenario while ignoring the base rate of similar past projects. When engineers estimate, they imagine how the task will go when everything works — no unexpected dependencies, no design dead-ends, no scope creep, no production incidents interrupting work.",[20,7346,7347],{},"This isn't wishful thinking consciously. It's the brain generating a narrative of task completion without adequately accounting for the class of things that might go wrong.",[5756,7349,7351],{"id":7350},"unknown-unknowns","Unknown Unknowns",[20,7353,7354],{},"Estimation works reasonably well for work you've done before. The problem with software is that genuinely novel work — a new integration, an unfamiliar codebase, a problem you haven't encountered — has unknown unknowns. You don't know what you don't know until you encounter it.",[20,7356,7357],{},"Discovery work, exploratory design, and legacy system interaction are particularly estimation-resistant for this reason. Every time you think you understand the scope, you find another layer.",[5756,7359,7361],{"id":7360},"requirements-drift","Requirements Drift",[20,7363,7364],{},"A task estimated as three days grows to ten days because the requirements evolved during development. This is often counted as an estimation failure when it's actually a requirements failure. The estimate was for a different scope than what was eventually built.",[5756,7366,7368],{"id":7367},"optimistic-completion-rates","Optimistic Completion Rates",[20,7370,7371],{},"Engineers typically estimate how long a task takes when they're working on it — not accounting for meetings, context switching, code review cycles, deployment pipelines, and the reality that a \"day of work\" in most engineering organizations is three to five focused hours.",[27,7373],{},[15,7375,7377],{"id":7376},"the-cone-of-uncertainty","The Cone of Uncertainty",[20,7379,7380],{},"The cone of uncertainty is one of the most useful frameworks for communicating honest estimates. Developed by Barry Boehm and formalized in Steve McConnell's work on software estimation, it quantifies the range of possible outcomes at different stages of a project.",[20,7382,7383],{},"At project initiation, before detailed requirements are understood, an estimate might be off by a factor of 4x in either direction — what looks like a 6-month project might take anywhere from 1.5 to 24 months. As requirements are detailed and a high-level design is produced, the range narrows to roughly 1.5x in either direction. After a detailed design with specification of individual components, the range narrows further.",[20,7385,7386],{},"The cone of uncertainty is not pessimism. It's an accurate representation of what estimates mean at different stages. An estimate given before requirements are understood isn't really an estimate — it's an order-of-magnitude guess, and it should be communicated as such.",[5756,7388,7390],{"id":7389},"communicating-with-the-cone","Communicating With the Cone",[20,7392,7393],{},"When someone asks for an estimate before the work is fully understood, give them a range, not a point estimate. \"Based on similar work we've done, this is likely 3-6 weeks. Once we've done a spike to understand the integration complexity, I can give you a tighter range.\"",[20,7395,7396],{},"This communication is more honest, more useful, and more likely to result in good project planning than a falsely precise single number.",[27,7398],{},[15,7400,7402],{"id":7401},"reference-class-forecasting","Reference Class Forecasting",[20,7404,7405],{},"Reference class forecasting is a technique developed by psychologists to counter the planning fallacy. Instead of estimating from the inside view (how this specific task will go), you estimate from the outside view (how tasks like this typically go).",[20,7407,7408],{},"The process:",[2070,7410,7411,7414,7417,7420],{},[188,7412,7413],{},"Identify a reference class of similar past projects or tasks",[188,7415,7416],{},"Determine the historical distribution of outcomes for that class",[188,7418,7419],{},"Anchor your estimate on the historical distribution",[188,7421,7422],{},"Adjust (modestly) for features of this specific case that distinguish it from the class",[20,7424,7425],{},"In practice, this means keeping records. How long did similar past features take? What percentage of them came in on time? What was the average overrun factor? If your team's last five integration projects averaged 1.8x the initial estimate, your next integration estimate should probably be multiplied by 1.8.",[20,7427,7428],{},"Teams that track their estimation accuracy systematically — comparing estimates to actuals across a meaningful sample of work — are much better at estimating than teams that don't. The feedback loop is what calibrates intuition.",[27,7430],{},[15,7432,7434],{"id":7433},"estimation-techniques-in-practice","Estimation Techniques in Practice",[5756,7436,7438],{"id":7437},"story-points-and-relative-estimation","Story Points and Relative Estimation",[20,7440,7441],{},"Agile teams often use story points to estimate relative complexity rather than time. A task assigned 3 points is roughly three times more complex than a 1-point task. Over time, the team's \"velocity\" (points completed per sprint) provides a throughput metric that can be used for capacity planning.",[20,7443,7444],{},"Story points sidestep the planning fallacy somewhat by focusing on complexity rather than duration. They work reasonably well for teams with stable membership and reasonably consistent work types. They break down for novel work types, team changes, and long-range planning.",[5756,7446,7448],{"id":7447},"three-point-estimation-pert","Three-Point Estimation (PERT)",[20,7450,7451,7452,7455],{},"For individual tasks, three-point estimation provides a range: Best Case (B), Most Likely (M), and Worst Case (W). Expected duration is calculated as ",[287,7453,7454],{},"(B + 4M + W) / 6",". This forces explicit consideration of the pessimistic case, which planning fallacy-prone estimators tend to ignore.",[5756,7457,7459],{"id":7458},"decomposition","Decomposition",[20,7461,7462],{},"Break work into the smallest pieces you can before estimating. Large tasks are estimated poorly because they contain many unknown unknowns. Small, well-understood tasks are estimated better. If you can't break a task down below a week, that's a signal that the scope isn't understood well enough to estimate.",[5756,7464,7466],{"id":7465},"time-boxing-discovery-work","Time-Boxing Discovery Work",[20,7468,7469],{},"For genuinely novel or exploratory work, don't estimate it — time-box it. \"We'll spend three days investigating the feasibility of this integration and come back with a better-scoped estimate.\" A time-boxed spike gives you the information you need to estimate the actual implementation without creating a false precision estimate upfront.",[27,7471],{},[15,7473,7475],{"id":7474},"estimation-anti-patterns","Estimation Anti-Patterns",[20,7477,7478,7481],{},[36,7479,7480],{},"Negotiating estimates."," If a manager responds to an estimate by saying \"that can't be right, we need it done in two weeks\" and the engineer revises their estimate to two weeks, the estimate has become a commitment to deliver under the manager's preferred schedule. The underlying complexity hasn't changed. The outcome will be either low quality, burnout, or a missed commitment.",[20,7483,7484,7487],{},[36,7485,7486],{},"Padding without communicating."," Engineers who've learned that estimates get negotiated down start padding them. This is rational but leads to inflated backlogs, poor prioritization, and the erosion of trust when padding is detected. The better approach: provide honest estimates with explicit uncertainty ranges.",[20,7489,7490,7493],{},[36,7491,7492],{},"Estimating without historical data."," Making predictions without tracking outcomes produces no feedback loop. You have no way to know if your estimates are systematically optimistic, pessimistic, or inconsistent.",[20,7495,7496,7499],{},[36,7497,7498],{},"Ignoring task dependencies."," Estimates for individual tasks don't account for sequencing, blocking dependencies, or critical path delays. Project timeline estimates need to model dependencies, not just sum task estimates.",[27,7501],{},[15,7503,7505],{"id":7504},"what-good-estimation-practice-looks-like","What Good Estimation Practice Looks Like",[20,7507,7508],{},"Good estimation practice in a software team looks like:",[185,7510,7511,7514,7517,7520,7523,7526],{},[188,7512,7513],{},"Maintaining historical records of estimate vs actual for a meaningful sample of work",[188,7515,7516],{},"Communicating estimates as ranges with explicit confidence levels",[188,7518,7519],{},"Time-boxing discovery before estimating novel work",[188,7521,7522],{},"Decomposing before estimating",[188,7524,7525],{},"Treating requirements changes as scope changes that require estimate revision",[188,7527,7528],{},"Building in buffer for integration, testing, and code review — not just implementation",[20,7530,7531],{},"Estimation will never be a precise science. The goal is calibrated estimates — estimates whose uncertainty is accurate and whose track record is trustworthy. A team that consistently delivers within their estimated range, even if the range is wide, is doing something very valuable.",[27,7533],{},[20,7535,7536,7537],{},"If you're working on improving estimation practices within an engineering team or building a planning process that's more honest about uncertainty, ",[171,7538,7540],{"href":173,"rel":7539},[175],"I'd be glad to connect.",[27,7542],{},[15,7544,183],{"id":182},[185,7546,7547,7551,7555,7559],{},[188,7548,7549],{},[171,7550,6261],{"href":6260},[188,7552,7553],{},[171,7554,6267],{"href":6266},[188,7556,7557],{},[171,7558,6255],{"href":6254},[188,7560,7561],{},[171,7562,7564],{"href":7563},"/blog/technical-roadmap-guide","Building a Technical Roadmap That Business Stakeholders Actually Trust",{"title":213,"searchDepth":214,"depth":214,"links":7566},[7567,7568,7574,7577,7578,7584,7585,7586],{"id":7314,"depth":217,"text":7315},{"id":7336,"depth":217,"text":7337,"children":7569},[7570,7571,7572,7573],{"id":7340,"depth":214,"text":7341},{"id":7350,"depth":214,"text":7351},{"id":7360,"depth":214,"text":7361},{"id":7367,"depth":214,"text":7368},{"id":7376,"depth":217,"text":7377,"children":7575},[7576],{"id":7389,"depth":214,"text":7390},{"id":7401,"depth":217,"text":7402},{"id":7433,"depth":217,"text":7434,"children":7579},[7580,7581,7582,7583],{"id":7437,"depth":214,"text":7438},{"id":7447,"depth":214,"text":7448},{"id":7458,"depth":214,"text":7459},{"id":7465,"depth":214,"text":7466},{"id":7474,"depth":217,"text":7475},{"id":7504,"depth":217,"text":7505},{"id":182,"depth":217,"text":183},"Software estimation is notoriously inaccurate — but not because engineers are bad at math. Here's why estimation fails and the techniques that actually make it more reliable in practice.",[7589,7590,7591,7592,7593],"software estimation","software estimation techniques","how to estimate software projects","cone of uncertainty","reference class forecasting",{},"/blog/software-estimation-techniques",{"title":7308,"description":7587},"blog/software-estimation-techniques",[7599,7600,7601,1116],"Software Estimation","Engineering Leadership","Project Planning","HdHfMNFxF6j9rjgFbYaMVWzqACWxfie5VbLr9eKK6O0",[7604,7606,7608,7610,7611,7612,7613,7614,7615,7616,7617,7618,7619,7620,7621,7622,7623,7624,7625,7626,7627,7628,7629,7630,7631,7632,7633,7634,7635,7636,7637,7638,7639,7640,7641,7642,7643,7644,7645,7646,7647,7648,7649,7650,7651,7652,7653,7654,7655,7656,7657,7658,7659,7660,7661,7662,7663,7664,7665,7666,7667,7668,7669,7670,7671,7672,7673,7674,7675,7676,7677,7678,7679,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7958,7959,7960,7961,7962,7963,7964,7965,7966,7967,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8010,8011,8012,8013,8014,8015,8016,8017,8018,8019,8020,8021,8022,8023,8024,8025,8026,8027,8028,8029,8030,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8117,8118,8119,8120,8121,8122,8123,8124,8125,8126,8127,8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141,8142,8143,8144,8145,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155,8156,8157,8158,8159,8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8173,8174,8175,8176,8177,8178,8179,8180,8181,8182,8183,8184,8185,8186,8187,8188,8189,8190,8191,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207,8208,8209,8210,8211,8212,8213,8214,8215,8216,8217,8218,8219,8220,8221,8222,8223,8224,8225,8226,8227,8228,8229,8230,8231,8232,8233,8234,8235,8236,8237,8238,8239,8240,8241,8242,8243,8244,8245,8246,8247],{"category":7605},"Frontend",{"category":7607},"Heritage",{"category":7609},"AI",{"category":545},{"category":223},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7609},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":884},{"category":545},{"category":545},{"category":884},{"category":545},{"category":545},{"category":555},{"category":555},{"category":223},{"category":223},{"category":7607},{"category":555},{"category":7607},{"category":884},{"category":555},{"category":545},{"category":223},{"category":1983},{"category":7609},{"category":7607},{"category":545},{"category":884},{"category":545},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":545},{"category":884},{"category":545},{"category":545},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":1983},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":545},{"category":6293},{"category":7609},{"category":7609},{"category":223},{"category":884},{"category":223},{"category":545},{"category":545},{"category":223},{"category":545},{"category":884},{"category":545},{"category":1983},{"category":1983},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7609},{"category":884},{"category":223},{"category":1983},{"category":1983},{"category":1983},{"category":7607},{"category":545},{"category":545},{"category":7607},{"category":7605},{"category":7609},{"category":1983},{"category":1983},{"category":555},{"category":1983},{"category":223},{"category":7609},{"category":7607},{"category":545},{"category":7607},{"category":884},{"category":7607},{"category":884},{"category":555},{"category":7607},{"category":7607},{"category":545},{"category":223},{"category":545},{"category":7605},{"category":545},{"category":545},{"category":545},{"category":545},{"category":223},{"category":223},{"category":7607},{"category":7605},{"category":555},{"category":884},{"category":555},{"category":7605},{"category":545},{"category":545},{"category":1983},{"category":545},{"category":545},{"category":884},{"category":545},{"category":1983},{"category":545},{"category":545},{"category":7607},{"category":7607},{"category":555},{"category":884},{"category":884},{"category":6293},{"category":6293},{"category":6293},{"category":223},{"category":545},{"category":1983},{"category":884},{"category":7607},{"category":7607},{"category":1983},{"category":884},{"category":884},{"category":7605},{"category":545},{"category":7607},{"category":7607},{"category":545},{"category":7607},{"category":1983},{"category":1983},{"category":7607},{"category":555},{"category":7607},{"category":884},{"category":555},{"category":884},{"category":545},{"category":884},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":884},{"category":545},{"category":545},{"category":555},{"category":545},{"category":1983},{"category":1983},{"category":223},{"category":545},{"category":545},{"category":545},{"category":884},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":884},{"category":884},{"category":884},{"category":545},{"category":7607},{"category":7607},{"category":7607},{"category":1983},{"category":223},{"category":7607},{"category":7607},{"category":545},{"category":7607},{"category":545},{"category":7605},{"category":7607},{"category":223},{"category":223},{"category":545},{"category":545},{"category":7609},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":545},{"category":1983},{"category":1983},{"category":1983},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":7607},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":223},{"category":223},{"category":7607},{"category":545},{"category":7605},{"category":884},{"category":6293},{"category":7607},{"category":7607},{"category":555},{"category":545},{"category":7607},{"category":7607},{"category":1983},{"category":7607},{"category":7605},{"category":1983},{"category":1983},{"category":555},{"category":545},{"category":545},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":6293},{"category":7607},{"category":884},{"category":545},{"category":545},{"category":7607},{"category":1983},{"category":7607},{"category":7607},{"category":7607},{"category":7605},{"category":7607},{"category":7607},{"category":545},{"category":7607},{"category":545},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":7609},{"category":7609},{"category":545},{"category":7607},{"category":1983},{"category":1983},{"category":7607},{"category":545},{"category":7607},{"category":7607},{"category":7609},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":545},{"category":555},{"category":545},{"category":545},{"category":7605},{"category":545},{"category":7605},{"category":7605},{"category":555},{"category":884},{"category":545},{"category":884},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":545},{"category":223},{"category":545},{"category":545},{"category":7607},{"category":884},{"category":7609},{"category":7609},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":223},{"category":545},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":7605},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":545},{"category":884},{"category":545},{"category":545},{"category":545},{"category":884},{"category":7607},{"category":223},{"category":7609},{"category":7607},{"category":223},{"category":555},{"category":7607},{"category":555},{"category":545},{"category":1983},{"category":7607},{"category":7607},{"category":545},{"category":7607},{"category":884},{"category":7607},{"category":7607},{"category":545},{"category":223},{"category":545},{"category":545},{"category":545},{"category":545},{"category":223},{"category":545},{"category":545},{"category":223},{"category":1983},{"category":545},{"category":7609},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":7607},{"category":7607},{"category":7607},{"category":7609},{"category":545},{"category":545},{"category":884},{"category":7605},{"category":545},{"category":7607},{"category":545},{"category":884},{"category":223},{"category":223},{"category":7605},{"category":7605},{"category":7607},{"category":223},{"category":555},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":545},{"category":545},{"category":884},{"category":545},{"category":545},{"category":545},{"category":8078},"Programming",{"category":545},{"category":545},{"category":884},{"category":884},{"category":545},{"category":545},{"category":223},{"category":555},{"category":545},{"category":223},{"category":545},{"category":545},{"category":545},{"category":545},{"category":1983},{"category":884},{"category":223},{"category":223},{"category":545},{"category":545},{"category":223},{"category":545},{"category":555},{"category":223},{"category":545},{"category":545},{"category":884},{"category":884},{"category":7607},{"category":223},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":7605},{"category":7607},{"category":1983},{"category":555},{"category":555},{"category":555},{"category":555},{"category":555},{"category":555},{"category":7607},{"category":545},{"category":1983},{"category":884},{"category":1983},{"category":884},{"category":545},{"category":7605},{"category":7607},{"category":884},{"category":7605},{"category":7607},{"category":7607},{"category":7607},{"category":884},{"category":884},{"category":884},{"category":223},{"category":223},{"category":223},{"category":884},{"category":884},{"category":223},{"category":223},{"category":223},{"category":7607},{"category":555},{"category":545},{"category":1983},{"category":545},{"category":7607},{"category":223},{"category":223},{"category":7607},{"category":7607},{"category":884},{"category":545},{"category":884},{"category":884},{"category":884},{"category":7605},{"category":545},{"category":7607},{"category":7607},{"category":223},{"category":223},{"category":884},{"category":545},{"category":6293},{"category":884},{"category":6293},{"category":223},{"category":7607},{"category":884},{"category":7607},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":7607},{"category":7609},{"category":7609},{"category":1983},{"category":7607},{"category":7607},{"category":7607},{"category":7607},{"category":545},{"category":545},{"category":7605},{"category":545},{"category":555},{"category":884},{"category":7605},{"category":7605},{"category":545},{"category":545},{"category":7605},{"category":7605},{"category":7605},{"category":555},{"category":545},{"category":545},{"category":223},{"category":545},{"category":884},{"category":7607},{"category":7607},{"category":884},{"category":7607},{"category":7607},{"category":884},{"category":7607},{"category":545},{"category":7607},{"category":555},{"category":7607},{"category":7607},{"category":7607},{"category":1983},{"category":1983},{"category":555},1772951194541]