<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Domonique Luchin</title>
    <description>The latest articles on DEV Community by Domonique Luchin (@domoniqueluchin).</description>
    <link>https://dev.to/domoniqueluchin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3816446%2F43258dea-c4c5-4bc1-95b1-9e05955201f1.png</url>
      <title>DEV Community: Domonique Luchin</title>
      <link>https://dev.to/domoniqueluchin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/domoniqueluchin"/>
    <language>en</language>
    <item>
      <title>From I-9 to invoice: onboarding a W2 job while running 6 LLCs</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Wed, 17 Jun 2026 10:00:05 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/from-i-9-to-invoice-onboarding-a-w2-job-while-running-6-llcs-imh</link>
      <guid>https://dev.to/domoniqueluchin/from-i-9-to-invoice-onboarding-a-w2-job-while-running-6-llcs-imh</guid>
      <description>&lt;p&gt;I just started a new W2 position as Lead Structural Engineer at ESI Inc. while running Load Bearing Empire - 6 AI-powered businesses under my main LLC. The onboarding process got interesting fast.&lt;/p&gt;

&lt;p&gt;Here's what happens when HR meets entrepreneurship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paperwork maze
&lt;/h2&gt;

&lt;p&gt;Standard W2 onboarding is straightforward. Fill out your I-9, W4, direct deposit forms. Done in 30 minutes.&lt;/p&gt;

&lt;p&gt;Add 6 active LLCs and it becomes a disclosure minefield.&lt;/p&gt;

&lt;p&gt;My employment agreement had this clause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Employee shall not engage in any business activities that conflict with Company interests or duties."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I spent 2 hours documenting each business to prove zero conflict with structural engineering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Luchin Real Estate &amp;amp; Investments LLC (Parent)
├── Property management automation
├── AI phone answering service  
├── Document processing service
├── Lead generation platform
├── Real estate CRM
└── Investment analysis tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None compete with pipe rack design or foundation analysis. But you have to prove it upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tax complexity hits different
&lt;/h2&gt;

&lt;p&gt;Here's where it gets fun. My 2023 tax situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;W2 income: $89,000&lt;/li&gt;
&lt;li&gt;LLC business income: $47,000
&lt;/li&gt;
&lt;li&gt;Quarterly estimated taxes: $3,200&lt;/li&gt;
&lt;li&gt;Business expenses: $12,000&lt;/li&gt;
&lt;li&gt;Home office deduction: $1,500&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I'm juggling both again. My accountant charges $2,400/year just to sort through the mess.&lt;/p&gt;

&lt;p&gt;The new W2 changes my quarterly estimated tax calculations immediately. I had to recalculate within 2 weeks of starting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time blocking becomes critical
&lt;/h2&gt;

&lt;p&gt;40 hours per week for ESI. 25 hours per week for the LLCs. &lt;/p&gt;

&lt;p&gt;My schedule looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monday-Friday 8AM-5PM: Structural engineering
Monday-Friday 6PM-8PM: LLC operations  
Saturday 8AM-12PM: Development work
Sunday 2PM-5PM: Financial review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot wing this. Block your time or watch everything burn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure advantage
&lt;/h2&gt;

&lt;p&gt;Running my own tech stack saves me here. Everything runs on one $240/month Vultr VPS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asterisk PBX for business calls&lt;/li&gt;
&lt;li&gt;VAPI agents handling customer service&lt;/li&gt;
&lt;li&gt;Supabase for all databases
&lt;/li&gt;
&lt;li&gt;LangGraph orchestrating workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No SaaS subscriptions bleeding me dry. When you own your infrastructure, scaling doesn't murder your margins.&lt;/p&gt;

&lt;p&gt;My AI phone system processes 200+ calls per month automatically. That's 8 hours I don't spend on customer service while at my day job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Legal structure matters
&lt;/h2&gt;

&lt;p&gt;I structured everything under one parent LLC for good reason:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Luchin Real Estate &amp;amp; Investments LLC&lt;/strong&gt; owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All business bank accounts&lt;/li&gt;
&lt;li&gt;The VPS and infrastructure
&lt;/li&gt;
&lt;li&gt;Equipment and software licenses&lt;/li&gt;
&lt;li&gt;Real estate investments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates clean separation from my W2 employment. ESI pays Domonique Luchin the individual. The LLCs are separate legal entities.&lt;/p&gt;

&lt;p&gt;One EIN for taxes. One operating agreement. One registered agent fee ($125/year in Georgia).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash flow reality check
&lt;/h2&gt;

&lt;p&gt;Month 1 numbers with the new W2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W2 take-home (after taxes/benefits): $4,200
LLC net profit: $2,800
Total monthly cash: $7,000

Fixed business expenses: $890
Personal expenses: $3,200
Investment contributions: $1,500
Cash remaining: $1,410
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The W2 provides stability. The LLCs provide upside. You need both when you're building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Banking got weird fast.&lt;/strong&gt; I have 3 business accounts now. My bank wanted to understand why a structural engineer owns AI service companies. Took 2 meetings and documentation to sort out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insurance overlaps.&lt;/strong&gt; My W2 health insurance covers me. But I needed separate E&amp;amp;O insurance for the LLCs. My agent had never seen this combination before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Networking confusion.&lt;/strong&gt; Industry events don't know how to categorize me. Am I the engineer or the tech entrepreneur? I'm both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real talk
&lt;/h2&gt;

&lt;p&gt;Running 6 businesses while working full-time is not glamorous. It's spreadsheets at 10PM. It's taking customer calls during lunch breaks. It's explaining to your date why you check revenue numbers on Saturday morning.&lt;/p&gt;

&lt;p&gt;But it works if you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automate everything possible&lt;/li&gt;
&lt;li&gt;Own your infrastructure &lt;/li&gt;
&lt;li&gt;Structure legally from day one&lt;/li&gt;
&lt;li&gt;Track every dollar obsessively&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Your next move
&lt;/h2&gt;

&lt;p&gt;Pick one. Either commit to climbing the corporate ladder or start building your own thing. Doing both halfway gets you nowhere.&lt;/p&gt;

&lt;p&gt;If you're going to do both, document everything. Your future self will thank you when tax season arrives.&lt;/p&gt;

&lt;p&gt;What's your biggest challenge balancing W2 work with side businesses? Drop a comment below.&lt;/p&gt;

</description>
      <category>career</category>
      <category>entrepreneurship</category>
      <category>buildinpublic</category>
      <category>finance</category>
    </item>
    <item>
      <title>How I use Perplexity as an intelligence layer without giving it control</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Tue, 16 Jun 2026 10:00:05 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/how-i-use-perplexity-as-an-intelligence-layer-without-giving-it-control-1e7o</link>
      <guid>https://dev.to/domoniqueluchin/how-i-use-perplexity-as-an-intelligence-layer-without-giving-it-control-1e7o</guid>
      <description>&lt;p&gt;I run 6 AI-powered businesses on a single VPS. Every day I need fast, accurate research to make engineering decisions and validate business concepts. &lt;/p&gt;

&lt;p&gt;Perplexity gives me that intelligence without taking control of my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The difference between intelligence and control
&lt;/h2&gt;

&lt;p&gt;Most people hand over their entire research process to AI tools. They ask a question and accept whatever comes back.&lt;/p&gt;

&lt;p&gt;I do the opposite. I use Perplexity as a research assistant that feeds into my own decision-making systems.&lt;/p&gt;

&lt;p&gt;Here's my framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intelligence&lt;/strong&gt;: Getting facts, citations, and analysis quickly&lt;br&gt;
&lt;strong&gt;Control&lt;/strong&gt;: Making final decisions and taking action&lt;/p&gt;

&lt;p&gt;You want AI handling intelligence. You want humans keeping control.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Perplexity workflow for engineering decisions
&lt;/h2&gt;

&lt;p&gt;When I'm designing pipe racks or equipment platforms, I need current code requirements, material specs, and industry standards. Fast.&lt;/p&gt;

&lt;p&gt;Here's how I structure my Perplexity queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query: "AISC 360-22 connection requirements for HSS tube steel 
moment connections, wind load applications, cite specific sections"

Follow-up: "Compare this to AISC 341-22 seismic provisions 
for same connection type"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I get back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specific code sections with citations&lt;/li&gt;
&lt;li&gt;Multiple source verification&lt;/li&gt;
&lt;li&gt;Clear technical specifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here's the key: I never ask Perplexity to make the engineering judgment. That stays with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research validation system
&lt;/h2&gt;

&lt;p&gt;I built a simple validation process around Perplexity's output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check sources&lt;/strong&gt;: I verify at least 2 of the cited sources directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code confirmation&lt;/strong&gt;: I pull up the actual engineering codes referenced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity test&lt;/strong&gt;: Does this align with my 6+ years of O&amp;amp;G experience?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This takes 5 extra minutes but saves hours of potential rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business intelligence without business control
&lt;/h2&gt;

&lt;p&gt;For my Load Bearing Empire businesses, I use Perplexity to research market conditions and competitor analysis.&lt;/p&gt;

&lt;p&gt;Example query structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Commercial real estate cap rates Q3 2024 Atlanta metro, 
focus on industrial properties under $2M, include data sources"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I get market data in 30 seconds instead of spending 2 hours digging through reports.&lt;/p&gt;

&lt;p&gt;But I don't ask: "Should I buy this property?"&lt;/p&gt;

&lt;p&gt;That decision requires my financial analysis, risk assessment, and local market knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration with my AI stack
&lt;/h2&gt;

&lt;p&gt;My VPS runs Asterisk PBX with VAPI agents that handle initial client calls. When these agents need technical information, they query a knowledge base I populate using Perplexity research.&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client asks about structural engineering services&lt;/li&gt;
&lt;li&gt;Agent checks internal knowledge base&lt;/li&gt;
&lt;li&gt;If gaps exist, I research with Perplexity&lt;/li&gt;
&lt;li&gt;I validate and add verified info to knowledge base&lt;/li&gt;
&lt;li&gt;Agent can now handle similar questions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agents never directly query Perplexity during client calls. They only access pre-validated information I've approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research categories that work best
&lt;/h2&gt;

&lt;p&gt;I get the most value from Perplexity in these areas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Example Use&lt;/th&gt;
&lt;th&gt;Validation Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technical codes&lt;/td&gt;
&lt;td&gt;AISC, IBC updates&lt;/td&gt;
&lt;td&gt;Direct code verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market data&lt;/td&gt;
&lt;td&gt;Real estate comps&lt;/td&gt;
&lt;td&gt;MLS cross-check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Industry trends&lt;/td&gt;
&lt;td&gt;Construction pricing&lt;/td&gt;
&lt;td&gt;Vendor confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competitive intel&lt;/td&gt;
&lt;td&gt;Service offerings&lt;/td&gt;
&lt;td&gt;Direct website review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I don't use Perplexity for
&lt;/h2&gt;

&lt;p&gt;Some things require human judgment or proprietary knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client-specific engineering calculations&lt;/li&gt;
&lt;li&gt;Financial projections for my businesses
&lt;/li&gt;
&lt;li&gt;Strategic business decisions&lt;/li&gt;
&lt;li&gt;Quality control on engineering drawings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These stay in-house using my own tools and experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost-benefit equation
&lt;/h2&gt;

&lt;p&gt;I pay $20/month for Perplexity Pro. This replaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple industry publication subscriptions ($200+/month)&lt;/li&gt;
&lt;li&gt;Research assistant time (10+ hours/week)&lt;/li&gt;
&lt;li&gt;Database access fees for market research&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But more importantly, it speeds up my decision-making cycle by 3-4x while keeping me in control.&lt;/p&gt;

&lt;h2&gt;
  
  
  My filtering system
&lt;/h2&gt;

&lt;p&gt;Not all Perplexity responses are equal. I filter based on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source quality&lt;/strong&gt;: Academic papers, government data, and industry publications rank highest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citation depth&lt;/strong&gt;: Responses with 5+ relevant sources get priority&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recency&lt;/strong&gt;: For technical codes and market data, I need information from the last 12 months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specificity&lt;/strong&gt;: Vague responses get re-queried with more precise language&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The infrastructure ownership principle
&lt;/h2&gt;

&lt;p&gt;This fits my philosophy of owning infrastructure instead of depending on SaaS subscriptions.&lt;/p&gt;

&lt;p&gt;I use Perplexity as an input to systems I control, not as a replacement for my judgment. The research flows into my Supabase database, gets processed through my Python scripts, and feeds my decision-making frameworks.&lt;/p&gt;

&lt;p&gt;The intelligence comes from outside. The control stays with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with boundaries
&lt;/h2&gt;

&lt;p&gt;If you want to try this approach, define your boundaries first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What decisions will you never delegate to AI?&lt;/li&gt;
&lt;li&gt;How will you validate AI-generated research?&lt;/li&gt;
&lt;li&gt;Where does AI research fit in your existing workflow?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Answer these before you start. You'll avoid the trap of gradually handing over control without realizing it.&lt;/p&gt;

&lt;p&gt;The goal isn't to replace your judgment. It's to feed your judgment with better information, faster.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>research</category>
      <category>agents</category>
      <category>tools</category>
    </item>
    <item>
      <title>How I automated FCRA credit dispute letters with Supabase and edge functions</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Mon, 15 Jun 2026 10:00:04 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/how-i-automated-fcra-credit-dispute-letters-with-supabase-and-edge-functions-3n10</link>
      <guid>https://dev.to/domoniqueluchin/how-i-automated-fcra-credit-dispute-letters-with-supabase-and-edge-functions-3n10</guid>
      <description>&lt;p&gt;I needed to help clients dispute credit report errors. Writing FCRA dispute letters manually took 30 minutes per letter. With 50+ clients, that's 25 hours of repetitive work monthly.&lt;/p&gt;

&lt;p&gt;I built an automated system using Supabase edge functions that generates personalized dispute letters in under 5 seconds. Here's exactly how I did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with manual dispute letters
&lt;/h2&gt;

&lt;p&gt;Every FCRA dispute letter needs specific elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client personal information&lt;/li&gt;
&lt;li&gt;Account details from credit reports&lt;/li&gt;
&lt;li&gt;Legal language citing Fair Credit Reporting Act sections&lt;/li&gt;
&lt;li&gt;Proper formatting for credit bureaus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was copying and pasting the same templates, changing names and account numbers. Human error crept in. Clients waited days for their letters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Supabase edge functions
&lt;/h2&gt;

&lt;p&gt;I already run my business infrastructure on a single Vultr VPS. Adding another SaaS subscription goes against my philosophy of infrastructure ownership.&lt;/p&gt;

&lt;p&gt;Supabase edge functions gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-side processing for sensitive data&lt;/li&gt;
&lt;li&gt;Built-in database integration&lt;/li&gt;
&lt;li&gt;No cold starts (important for client-facing tools)&lt;/li&gt;
&lt;li&gt;Full control over my stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Database schema design
&lt;/h2&gt;

&lt;p&gt;I created three main tables in Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Client information&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;zip_code&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ssn_last_four&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Dispute items&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dispute_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;creditor_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;account_number&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dispute_reason&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;bureau&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- Experian, Equifax, TransUnion&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Generated letters&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;generated_letters&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;letter_content&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;bureau&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item_count&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The edge function
&lt;/h2&gt;

&lt;p&gt;My edge function pulls client data, formats dispute items, and generates the complete letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://deno.land/std@0.168.0/http/server.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://esm.sh/@supabase/supabase-js@2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Method not allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bureau&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Get client info&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Get dispute items for this bureau&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dispute_items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bureau&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;letterContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateFCRALetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Save generated letter&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generated_letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;letter_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;letterContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;item_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;letterContent&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The letter generation logic
&lt;/h2&gt;

&lt;p&gt;I created templates for each section with dynamic content insertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateFCRALetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bureauAddresses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Experian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P.O. Box 4500, Allen, TX 75013&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Equifax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P.O. Box 740256, Atlanta, GA 30374&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TransUnion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P.O. Box 2000, Chester, PA 19016&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bureauAddresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;

Re: Request for Investigation of Credit Report Information
Name: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Address: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip_code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
SSN: ***-**-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssn_last_four&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

Dear &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Credit Bureau,

I am writing to dispute the following information on my credit report pursuant to my rights under the Fair Credit Reporting Act (15 U.S.C. §1681i).

ITEMS BEING DISPUTED:
`&lt;/span&gt;

  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Creditor: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;creditor_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
   Account Number: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
   Reason for Dispute: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispute_reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`
I request that you investigate these items and remove any information that cannot be verified as accurate and complete.

Please provide me with written results of your investigation within 30 days as required by law.

Sincerely,
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results that matter
&lt;/h2&gt;

&lt;p&gt;Since implementing this system 6 months ago:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Letter generation time: 30 minutes → 5 seconds&lt;/li&gt;
&lt;li&gt;Monthly time saved: 25 hours&lt;/li&gt;
&lt;li&gt;Error rate: Dropped to zero (no more copy-paste mistakes)&lt;/li&gt;
&lt;li&gt;Client satisfaction: Improved due to same-day turnaround&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I process 200+ dispute letters monthly now. The system handles peak loads without issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost comparison
&lt;/h2&gt;

&lt;p&gt;My total monthly costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supabase Pro: $25&lt;/li&gt;
&lt;li&gt;Vultr VPS: $12&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Equivalent services would cost $200+ monthly with traditional SaaS providers. You get vendor lock-in and data restrictions too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps for you
&lt;/h2&gt;

&lt;p&gt;If you're handling repetitive document generation, start with Supabase edge functions. The PostgreSQL integration makes complex data relationships simple.&lt;/p&gt;

&lt;p&gt;Set up your database schema first. Build templates that work manually before automating. Test with real data, not sample data.&lt;/p&gt;

&lt;p&gt;Your clients will notice the speed difference. You'll get your evenings back.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>fintech</category>
      <category>supabase</category>
      <category>python</category>
    </item>
    <item>
      <title>Building a royalty acquisition pipeline for oil and gas with AI agents</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Tue, 09 Jun 2026 10:00:05 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/building-a-royalty-acquisition-pipeline-for-oil-and-gas-with-ai-agents-554k</link>
      <guid>https://dev.to/domoniqueluchin/building-a-royalty-acquisition-pipeline-for-oil-and-gas-with-ai-agents-554k</guid>
      <description>&lt;p&gt;I've spent 6 years designing structural systems for oil and gas facilities. Pipe racks, equipment platforms, compressor stations. You learn to spot patterns in the industry data.&lt;/p&gt;

&lt;p&gt;Last year I started Load Bearing Empire to apply that pattern recognition to royalty acquisitions. Instead of building platforms, I'm building pipelines that find undervalued mineral rights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with traditional royalty hunting
&lt;/h2&gt;

&lt;p&gt;Most investors rely on brokers or cold calling landowners. You're competing with everyone else for the same deals. The good opportunities get bid up fast.&lt;/p&gt;

&lt;p&gt;I wanted first access to properties before they hit the market. That meant monitoring public records, permit databases, and production reports across multiple states. Doing this manually would take 40+ hours per week.&lt;/p&gt;

&lt;h2&gt;
  
  
  My AI agent architecture
&lt;/h2&gt;

&lt;p&gt;I built a system of specialized agents that work together on a single Vultr VPS. Each agent handles one piece of the acquisition process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scout Agent&lt;/strong&gt;: Monitors new drilling permits and lease filings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyst Agent&lt;/strong&gt;: Evaluates production data and calculates fair market values
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outreach Agent&lt;/strong&gt;: Contacts property owners through voice calls and SMS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deal Agent&lt;/strong&gt;: Manages negotiations and document review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents communicate through a shared Supabase database. LangGraph orchestrates the workflows between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scout Agent implementation
&lt;/h2&gt;

&lt;p&gt;The Scout Agent checks 3 data sources every morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;supabase&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_client&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScoutAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUPABASE_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_new_permits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Texas RRC API
&lt;/span&gt;        &lt;span class="n"&gt;permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_rrc_permits&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Oklahoma Corporation Commission
&lt;/span&gt;        &lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_occ_permits&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;# North Dakota Industrial Commission  
&lt;/span&gt;        &lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_ndic_permits&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;permit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meets_criteria&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_lead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;meets_criteria&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;operator_tier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tier_1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tier_2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
                &lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;formation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Eagle Ford&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Permian&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bakken&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
                &lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;estimated_reserves&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Scout Agent found 847 new permits last month. 23 met my screening criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production analysis with the Analyst Agent
&lt;/h2&gt;

&lt;p&gt;When the Scout Agent flags a property, the Analyst Agent pulls historical production data and runs a DCF model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_npv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;well_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;monthly_production&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;well_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;oil_bbls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;gas_production&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;well_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gas_mcf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Apply decline curve analysis
&lt;/span&gt;    &lt;span class="n"&gt;decline_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;estimate_decline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly_production&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;oil_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;  &lt;span class="c1"&gt;# Current WTI assumption
&lt;/span&gt;    &lt;span class="n"&gt;gas_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.2&lt;/span&gt;  &lt;span class="c1"&gt;# Henry Hub assumption
&lt;/span&gt;
    &lt;span class="n"&gt;cash_flows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# 20 year projection
&lt;/span&gt;        &lt;span class="n"&gt;oil_vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monthly_production&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;decline_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;
        &lt;span class="n"&gt;gas_vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gas_production&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;decline_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;

        &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oil_vol&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;oil_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gas_vol&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;gas_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;royalty_payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.125&lt;/span&gt;  &lt;span class="c1"&gt;# 1/8 royalty
&lt;/span&gt;        &lt;span class="n"&gt;cash_flows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;royalty_payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;npv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cash_flows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Analyst Agent valued 18 properties last month. Average NPV was $340,000 per mineral acre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated outreach that works
&lt;/h2&gt;

&lt;p&gt;Here's where most people mess up. They send generic emails that get ignored. I use VAPI voice agents running on my self-hosted Asterisk PBX.&lt;/p&gt;

&lt;p&gt;The Outreach Agent makes actual phone calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutreachAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vapi_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VAPIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VAPI_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUPABASE_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact_owner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;call_script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vapi_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_call&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner_phone&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;assistant_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mineral_rights_assistant&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;script_variables&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;property_description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;legal_description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;offer_range&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_offer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_offer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_interaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;property_record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The voice agent handles initial conversations. If the owner shows interest, it schedules a follow-up call with me.&lt;/p&gt;

&lt;p&gt;Response rate: 12% compared to 1-2% for cold emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results after 8 months
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Properties analyzed&lt;/td&gt;
&lt;td&gt;847&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qualified leads&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner contacts made&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interested responses&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deals closed&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total acquisition cost&lt;/td&gt;
&lt;td&gt;$2.1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projected 20-year NPV&lt;/td&gt;
&lt;td&gt;$4.8M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The system runs entirely on a $80/month Vultr VPS. No expensive SaaS subscriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can build
&lt;/h2&gt;

&lt;p&gt;You don't need oil and gas experience to use this approach. The same pattern works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distressed property acquisitions&lt;/li&gt;
&lt;li&gt;Commercial real estate deals
&lt;/li&gt;
&lt;li&gt;Land development opportunities&lt;/li&gt;
&lt;li&gt;Tax lien investments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is finding public data sources in your market and building agents that monitor them consistently.&lt;/p&gt;

&lt;p&gt;Start with one data source and one agent. Add complexity as you prove the concept works.&lt;/p&gt;

&lt;p&gt;You can find the code examples and setup guides at &lt;a href="https://loadbearingempire.com" rel="noopener noreferrer"&gt;loadbearingempire.com&lt;/a&gt;. I'm also building out tutorials for different real estate verticals.&lt;/p&gt;

&lt;p&gt;What deals are you missing because you're not monitoring the right data sources?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>realestate</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>The Infrastructure Sovereignty Framework: 5 Rules for Owning Your Stack</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Mon, 08 Jun 2026 10:00:04 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/the-infrastructure-sovereignty-framework-5-rules-for-owning-your-stack-5m2</link>
      <guid>https://dev.to/domoniqueluchin/the-infrastructure-sovereignty-framework-5-rules-for-owning-your-stack-5m2</guid>
      <description>&lt;p&gt;I run 6 AI-powered businesses on a single $40/month Vultr VPS. My monthly SaaS bill is $127. Compare that to entrepreneurs burning $2,000+ monthly on Vercel, AWS Lambda, and managed databases.&lt;/p&gt;

&lt;p&gt;The difference? Infrastructure sovereignty.&lt;/p&gt;

&lt;p&gt;After 6 years building oil and gas platforms, I apply the same ownership principles to digital infrastructure. You wouldn't rent the foundation for your office building. Why rent the foundation for your business?&lt;/p&gt;

&lt;p&gt;Here's my framework for owning your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: Own Your Compute Layer
&lt;/h2&gt;

&lt;p&gt;Your server is your land. Everything else is built on top.&lt;/p&gt;

&lt;p&gt;I host my entire Load Bearing Empire on one VPS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 vCPU, 8GB RAM&lt;/li&gt;
&lt;li&gt;Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;Docker containers for isolation&lt;/li&gt;
&lt;li&gt;Nginx for reverse proxy
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# My server setup script&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; get-docker.sh
sh get-docker.sh
docker network create empire-network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You control updates. You control security. You control costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your move&lt;/strong&gt;: Pick a provider (Vultr, Linode, Hetzner) and spin up your first VPS this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: Self-Host Your Database
&lt;/h2&gt;

&lt;p&gt;Databases are the crown jewels of your business. Why hand them to strangers?&lt;/p&gt;

&lt;p&gt;I run PostgreSQL in Docker with automated backups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;empire&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;domonique&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backups:/backups&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My backup script runs daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;postgres pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; domonique empire &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /backups/backup_&lt;span class="nv"&gt;$DATE&lt;/span&gt;.sql
find /backups &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.sql"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +7 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your move&lt;/strong&gt;: Install PostgreSQL locally. Practice backups and restores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: Control Your Communication Stack
&lt;/h2&gt;

&lt;p&gt;I built my own phone system using Asterisk PBX. My AI agents make calls through my infrastructure, not Twilio's.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# extensions.conf snippet
&lt;/span&gt;[&lt;span class="n"&gt;default&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;NXXNXXXXXX&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;trunk&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;NXXNXXXXXX&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()

&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Answer&lt;/span&gt;()
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Playback&lt;/span&gt;(&lt;span class="n"&gt;welcome&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;AGI&lt;/span&gt;(&lt;span class="n"&gt;vapi&lt;/span&gt;-&lt;span class="n"&gt;connector&lt;/span&gt;.&lt;span class="n"&gt;py&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.015/minute vs $0.085/minute on Twilio&lt;/li&gt;
&lt;li&gt;Custom call routing&lt;/li&gt;
&lt;li&gt;No vendor lock-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your move&lt;/strong&gt;: Set up a basic Asterisk server. Start with local extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: Build on Open Standards
&lt;/h2&gt;

&lt;p&gt;Proprietary APIs create dependencies. Open standards create freedom.&lt;/p&gt;

&lt;p&gt;My tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL (not DynamoDB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt;: Redis (not AWS SQS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: Elasticsearch (not Algolia)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: MinIO (not S3)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# My database abstraction
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmpireDB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you build on open standards, you can move anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your move&lt;/strong&gt;: Replace one proprietary service with an open source alternative this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: Automate Everything
&lt;/h2&gt;

&lt;p&gt;Manual processes don't scale. Your infrastructure should run itself.&lt;/p&gt;

&lt;p&gt;My monitoring stack sends alerts to my Slack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# health_check.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_services&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nginx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asterisk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;systemctl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is-active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
                               &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLACK_WEBHOOK&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;check_services&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs every 5 minutes via cron.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your move&lt;/strong&gt;: Write one automation script this week. Start small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Numbers
&lt;/h2&gt;

&lt;p&gt;Here's what sovereignty costs me monthly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Self-Hosted&lt;/th&gt;
&lt;th&gt;SaaS Alternative&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;td&gt;$200 (Vercel Pro)&lt;/td&gt;
&lt;td&gt;$160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$50 (PlanetScale)&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;$150 (Twilio)&lt;/td&gt;
&lt;td&gt;$125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$30 (AWS S3)&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$430&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$360&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's $4,320 saved per year. Real money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Next Step
&lt;/h2&gt;

&lt;p&gt;Pick Rule 1. Rent a VPS today. Install Ubuntu. Set up SSH keys. &lt;/p&gt;

&lt;p&gt;You don't need to migrate everything overnight. Start with a test project. Learn the basics. Build confidence.&lt;/p&gt;

&lt;p&gt;Infrastructure sovereignty isn't about rejecting all SaaS. It's about choosing dependence deliberately instead of drifting into it accidentally.&lt;/p&gt;

&lt;p&gt;Own your foundation. Everything else becomes possible.&lt;/p&gt;

&lt;p&gt;What's stopping you from owning your first server?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>selfhosted</category>
      <category>entrepreneurship</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Setting boundary conditions in RISA-3D: what the manual does not tell you</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Wed, 03 Jun 2026 10:00:02 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/setting-boundary-conditions-in-risa-3d-what-the-manual-does-not-tell-you-7fa</link>
      <guid>https://dev.to/domoniqueluchin/setting-boundary-conditions-in-risa-3d-what-the-manual-does-not-tell-you-7fa</guid>
      <description>&lt;p&gt;After six years of designing pipe racks and equipment platforms in the oil and gas industry, I've learned that RISA-3D's boundary condition setup can make or break your structural analysis. The manual gives you the basics, but it doesn't tell you about the gotchas that cost hours of debugging.&lt;/p&gt;

&lt;p&gt;Here's what I wish someone had taught me when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  The foundation modeling trap
&lt;/h2&gt;

&lt;p&gt;Most engineers set foundation boundary conditions like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node 1: Fx=Fixed, Fy=Fixed, Fz=Fixed, Mx=Fixed, My=Fixed, Mz=Fixed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for simple cases. But when you're modeling a 200-foot pipe rack with 40 foundations, fully fixed conditions create artificial stress concentrations that don't exist in reality.&lt;/p&gt;

&lt;p&gt;Real foundations have some flexibility. Even concrete pads on good soil rotate slightly under load.&lt;/p&gt;

&lt;p&gt;I model most spread footings like this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node 1: Fx=Fixed, Fy=Fixed, Fz=Fixed, Mx=Spring, My=Spring, Mz=Fixed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spring constants depend on your soil conditions and foundation size. For a typical 8'x8' concrete pad on medium clay, I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mx spring: 50,000 kip-ft/rad&lt;/li&gt;
&lt;li&gt;My spring: 50,000 kip-ft/rad&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces unrealistic moment concentrations at column bases by 30-40% in my experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pin connection mistake everyone makes
&lt;/h2&gt;

&lt;p&gt;RISA-3D defaults new members to "Fixed-Fixed" end conditions. You need to manually change these for realistic behavior.&lt;/p&gt;

&lt;p&gt;Bracing connections are almost never fully fixed in real structures. They're typically bolted with 2-4 bolts that can't transfer significant moments.&lt;/p&gt;

&lt;p&gt;For diagonal bracing, I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start: Pinned&lt;/li&gt;
&lt;li&gt;End: Pinned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For horizontal bracing between pipe rack bents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start: Fixed (welded to column)&lt;/li&gt;
&lt;li&gt;End: Pinned (bolted connection)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This change alone fixed convergence issues I was having on a recent platform design with 150+ diagonal braces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temperature effects that the manual ignores
&lt;/h2&gt;

&lt;p&gt;The RISA manual shows you how to apply thermal loads. It doesn't explain how boundary conditions interact with temperature changes.&lt;/p&gt;

&lt;p&gt;I learned this the hard way on a 300-foot pipe rack in Texas. Summer temperatures reach 105°F, winter can hit 20°F. That's an 85°F swing.&lt;/p&gt;

&lt;p&gt;With all foundations fully fixed, thermal expansion created massive fictitious forces. Some columns showed 200+ kip compression just from thermal effects.&lt;/p&gt;

&lt;p&gt;The fix: release one end of long structures for thermal movement.&lt;/p&gt;

&lt;p&gt;For pipe racks longer than 150 feet, I model one end with sliding supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expansion end: Fx=Free, Fy=Fixed, Fz=Fixed, Mx=Spring, My=Spring, Mz=Fixed
Fixed end: Fx=Fixed, Fy=Fixed, Fz=Fixed, Mx=Spring, My=Spring, Mz=Fixed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches real construction where expansion joints or slotted bolt holes accommodate thermal movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load path debugging with boundary reactions
&lt;/h2&gt;

&lt;p&gt;When your model won't converge or gives weird results, check the boundary reactions first.&lt;/p&gt;

&lt;p&gt;In RISA-3D, go to Results → Joint Reactions → Boundary Reactions.&lt;/p&gt;

&lt;p&gt;Look for these red flags:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uplift at interior supports&lt;/strong&gt;: Usually means your load combinations are wrong or you have sign errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Massive horizontal reactions&lt;/strong&gt;: Often indicates restrained thermal expansion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbalanced reactions&lt;/strong&gt;: Total reactions should equal applied loads&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Last month I had a model where one foundation showed 500 kip uplift. Turned out I had accidentally applied wind loads as downward instead of lateral. The boundary reactions caught this immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The integration order trap
&lt;/h2&gt;

&lt;p&gt;This one's subtle but important for dynamic analysis.&lt;/p&gt;

&lt;p&gt;RISA-3D uses different integration orders for different member types. The default works for most static analysis, but dynamic problems need consistent integration.&lt;/p&gt;

&lt;p&gt;For seismic analysis of equipment platforms, I set all members to the same integration order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select all members (Ctrl+A)&lt;/li&gt;
&lt;li&gt;Properties → Member → Advanced&lt;/li&gt;
&lt;li&gt;Set Integration Order to 3 for all members&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures consistent mass distribution and mode shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soil-structure interaction reality check
&lt;/h2&gt;

&lt;p&gt;The manual treats soil springs as simple linear springs. Real soil behavior is more complex.&lt;/p&gt;

&lt;p&gt;For equipment platforms on pile foundations, I model the soil-structure interaction in stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1&lt;/strong&gt;: Fully fixed foundations for initial sizing&lt;br&gt;
&lt;strong&gt;Stage 2&lt;/strong&gt;: Replace with nonlinear springs based on pile analysis&lt;br&gt;
&lt;strong&gt;Stage 3&lt;/strong&gt;: Apply working loads and check serviceability&lt;/p&gt;

&lt;p&gt;The spring constants come from separate pile analysis in programs like LPILE. But even approximate springs give more realistic results than fully fixed conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick validation checklist
&lt;/h2&gt;

&lt;p&gt;Before finalizing any model, I run through this boundary condition checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Foundation springs match soil conditions&lt;/li&gt;
&lt;li&gt;[ ] Pin connections used where appropriate&lt;/li&gt;
&lt;li&gt;[ ] Thermal expansion accommodated for long structures&lt;/li&gt;
&lt;li&gt;[ ] Boundary reactions are reasonable&lt;/li&gt;
&lt;li&gt;[ ] No artificial stress concentrations at supports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can catch 80% of modeling errors by questioning whether your boundary conditions match the actual construction.&lt;/p&gt;

&lt;p&gt;Your turn: next time you set up a RISA model, try the foundation springs instead of fully fixed conditions. Compare the column base moments between the two approaches. You'll see the difference immediately.&lt;/p&gt;

&lt;p&gt;What boundary condition issues have you run into? Drop your questions in the comments.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>structural</category>
      <category>software</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I built StructCalc AI to replace $500/month structural software licenses</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Tue, 02 Jun 2026 10:00:04 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/how-i-built-structcalc-ai-to-replace-500month-structural-software-licenses-1c6n</link>
      <guid>https://dev.to/domoniqueluchin/how-i-built-structcalc-ai-to-replace-500month-structural-software-licenses-1c6n</guid>
      <description>&lt;p&gt;I was paying $6,000 per year for structural engineering software at my firm. RISA-3D, STAAD.Pro, ETABS - you know the drill. These tools work, but the subscription costs kept climbing while my needs stayed the same: calculate beam deflections, check steel member capacities, and generate quick reports.&lt;/p&gt;

&lt;p&gt;So I built StructCalc AI. It handles 80% of my routine calculations and costs me $47/month to run on a Vultr VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with engineering SaaS
&lt;/h2&gt;

&lt;p&gt;Most structural engineers accept these costs as "the way things are." But here's what bothered me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RISA-3D: $2,400/year for features I use 20% of the time&lt;/li&gt;
&lt;li&gt;STAAD.Pro: $1,800/year with clunky interfaces from 2005
&lt;/li&gt;
&lt;li&gt;ETABS: $1,200/year for seismic analysis I do quarterly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed basic beam analysis, column checks, and foundation sizing. Not advanced dynamic analysis or 3D modeling.&lt;/p&gt;

&lt;p&gt;The math hasn't changed. Steel yield strength is still 50 ksi. Concrete compression strength formulas are still the same. Why rent software to do calculations I learned in college?&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the MVP in 3 weeks
&lt;/h2&gt;

&lt;p&gt;I started with FastAPI and PostgreSQL. The goal: replace my most common calculations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BeamInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;  &lt;span class="c1"&gt;# feet
&lt;/span&gt;    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;    &lt;span class="c1"&gt;# kips/ft
&lt;/span&gt;    &lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# "steel" or "concrete"
&lt;/span&gt;    &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;   &lt;span class="c1"&gt;# "W12x26", etc.
&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/analyze/beam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_beam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BeamInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Simple beam deflection calculation
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;29000&lt;/span&gt;  &lt;span class="c1"&gt;# ksi
&lt;/span&gt;        &lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_section_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;max_deflection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;max_moment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_deflection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_deflection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_moment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_moment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;l_over_deflection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max_deflection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simply supported beam analysis&lt;/li&gt;
&lt;li&gt;Steel column capacity checks
&lt;/li&gt;
&lt;li&gt;Concrete footing sizing&lt;/li&gt;
&lt;li&gt;Wind load calculations per ASCE 7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No fancy UI. Just API endpoints that returned JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Claude for code generation
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting. I connected Claude API to generate custom calculation scripts on demand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLAUDE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/generate/calculation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_calculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a structural engineer. Generate Python code for 
    engineering calculations. Use only standard engineering formulas. 
    Return executable code with proper error handling.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-haiku-20240307&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Execute the generated code safely
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute_calculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I can ask: "Calculate the required reinforcement for a 20x20 concrete column with 150 kips axial load" and get working Python code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics work out
&lt;/h2&gt;

&lt;p&gt;My current monthly costs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vultr VPS (8GB RAM)&lt;/td&gt;
&lt;td&gt;$24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude API calls&lt;/td&gt;
&lt;td&gt;$18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database backup&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$47&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare that to my old software stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RISA-3D: $200/month&lt;/li&gt;
&lt;li&gt;STAAD.Pro: $150/month
&lt;/li&gt;
&lt;li&gt;ETABS: $100/month&lt;/li&gt;
&lt;li&gt;AutoCAD Structural: $80/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm saving $483 per month. That's $5,796 per year back in my pocket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned about AI in engineering
&lt;/h2&gt;

&lt;p&gt;The AI doesn't replace engineering judgment. It automates the grunt work.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard beam/column calculations&lt;/li&gt;
&lt;li&gt;Code lookups (AISC, ACI)&lt;/li&gt;
&lt;li&gt;Generating boilerplate analysis code&lt;/li&gt;
&lt;li&gt;Quick "what-if" scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex seismic analysis&lt;/li&gt;
&lt;li&gt;Non-standard connection design&lt;/li&gt;
&lt;li&gt;Anything requiring engineering judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still use ETABS for major seismic projects. But for 80% of my work, StructCalc AI handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-hosting advantage
&lt;/h2&gt;

&lt;p&gt;Running everything on my VPS means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No vendor lock-in&lt;/li&gt;
&lt;li&gt;Full control of my data&lt;/li&gt;
&lt;li&gt;Custom integrations with my other tools&lt;/li&gt;
&lt;li&gt;No surprise price increases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I host it alongside my other Load Bearing Empire applications: the AI phone system, property management tools, and client portals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start small, build what you need
&lt;/h2&gt;

&lt;p&gt;You don't need to replace everything at once. Pick one expensive tool and build a targeted replacement.&lt;/p&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify your most expensive, least-used software&lt;/li&gt;
&lt;li&gt;List the 3-5 features you actually use&lt;/li&gt;
&lt;li&gt;Build those features with basic Python/FastAPI&lt;/li&gt;
&lt;li&gt;Add AI for the complex stuff&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The math is simple. If you're paying more than $50/month for software that does calculations, you can probably build a replacement.&lt;/p&gt;

&lt;p&gt;Want to see the code? I'm open-sourcing parts of StructCalc AI on GitHub. Follow me here for updates on the Load Bearing Empire stack and more engineering automation projects.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>ai</category>
      <category>webdev</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>Why structural engineers should learn Python before any other language</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:00:03 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/why-structural-engineers-should-learn-python-before-any-other-language-4efa</link>
      <guid>https://dev.to/domoniqueluchin/why-structural-engineers-should-learn-python-before-any-other-language-4efa</guid>
      <description>&lt;p&gt;I've been writing Python for three years now. Before that, I was just another structural engineer pushing numbers through RISA-3D and STAAD.Pro, wondering why simple calculations took so long.&lt;/p&gt;

&lt;p&gt;Python changed everything. Not because it's trendy. Because it solves real problems we face every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  You already think like a programmer
&lt;/h2&gt;

&lt;p&gt;As structural engineers, we break complex problems into smaller pieces. Load paths. Member checks. Connection design. This is exactly how programming works.&lt;/p&gt;

&lt;p&gt;When I design a pipe rack, I:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define loads and geometry&lt;/li&gt;
&lt;li&gt;Apply design codes and criteria
&lt;/li&gt;
&lt;li&gt;Check each member systematically&lt;/li&gt;
&lt;li&gt;Iterate until everything works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a perfect algorithm. You just need syntax to express it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python matches how we work
&lt;/h2&gt;

&lt;p&gt;Other languages force you into their world. Python adapts to yours.&lt;/p&gt;

&lt;p&gt;Need to process 500 load combinations? Python handles it.&lt;br&gt;
Want to read Excel files from clients? Python does that.&lt;br&gt;
Need to write back to Excel for deliverables? Python again.&lt;/p&gt;

&lt;p&gt;Here's how I automate a basic beam check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_beam_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moment_demand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section_modulus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check beam moment capacity per AISC 360&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;moment_capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;section_modulus&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;
    &lt;span class="n"&gt;dcr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;moment_demand&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;moment_capacity&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;capacity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;moment_capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dcr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dcr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dcr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Process multiple beams from Excel
&lt;/span&gt;&lt;span class="n"&gt;beams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_excel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;beam_data.xlsx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;beams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_beam_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;moment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;section_mod&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Write results back to Excel
&lt;/span&gt;&lt;span class="n"&gt;output_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;output_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_excel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;beam_results.xlsx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces hours of manual checking. You write it once, use it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real automation opportunities
&lt;/h2&gt;

&lt;p&gt;In six years of structural engineering, I've seen these tasks eat up time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load case generation&lt;/strong&gt;: Instead of manually creating 47 wind load cases, write a script. 15 minutes vs 3 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code checking&lt;/strong&gt;: Automate AISC 360 member checks. Your calculations become consistent and traceable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawing coordination&lt;/strong&gt;: Extract member sizes from analysis models, push to Excel, update drawings automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QC reviews&lt;/strong&gt;: Script your checking process. Catch errors the same way every time.&lt;/p&gt;

&lt;p&gt;I built a foundation design tool that takes soil reports and equipment loads, then generates complete calculations in 20 minutes. Before Python, this took half a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python integrates with everything
&lt;/h2&gt;

&lt;p&gt;Your existing tools already support Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STAAD.Pro&lt;/strong&gt;: OpenSTAAD API for model manipulation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ETABS&lt;/strong&gt;: Application programming interface for custom workflows
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excel&lt;/strong&gt;: pandas library reads/writes directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AutoCAD&lt;/strong&gt;: pyautocad for drawing automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases&lt;/strong&gt;: Connect to project management systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't replace your tools. You make them work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The learning curve is gentle
&lt;/h2&gt;

&lt;p&gt;Python reads like English. Compare this moment calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;moment_from_uniform_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="n"&gt;max_moment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment_from_uniform_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 2.5 k/ft, 24 ft span
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vs equivalent C++ code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cmath&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;momentFromUniformLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;maxMoment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;momentFromUniformLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxMoment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;endl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python gets out of your way. You focus on engineering, not syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with what you know
&lt;/h2&gt;

&lt;p&gt;Don't try to build the next STAAD.Pro. Start small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Week 1&lt;/strong&gt;: Learn basic Python syntax (variables, loops, functions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2&lt;/strong&gt;: Install pandas, read your first Excel file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3&lt;/strong&gt;: Write a simple code check (tension member, beam, column)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4&lt;/strong&gt;: Automate one repetitive task from your current project&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I started by automating wind load calculations. Saved 2 hours per project. That success motivated me to keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond day-to-day work
&lt;/h2&gt;

&lt;p&gt;Python opens doors you didn't know existed. I now run six businesses using Python-powered automation. My tools handle client calls, process data, and manage operations.&lt;/p&gt;

&lt;p&gt;This started with simple beam calculations three years ago.&lt;/p&gt;

&lt;p&gt;You don't need to become a software developer. But basic programming skills will make you a better engineer and create opportunities you can't imagine yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the first step
&lt;/h2&gt;

&lt;p&gt;Download Python today. Install pandas. Read one Excel file and print the contents.&lt;/p&gt;

&lt;p&gt;That's it. Everything else builds from there.&lt;/p&gt;

&lt;p&gt;You already solve complex problems every day. Python just gives you better tools to do it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>engineering</category>
      <category>career</category>
      <category>automation</category>
    </item>
    <item>
      <title>Reading Structural Drawings as an Engineer: The Coordination Review Workflow</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Mon, 25 May 2026 10:00:04 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/reading-structural-drawings-as-an-engineer-the-coordination-review-workflow-en5</link>
      <guid>https://dev.to/domoniqueluchin/reading-structural-drawings-as-an-engineer-the-coordination-review-workflow-en5</guid>
      <description>&lt;p&gt;I've reviewed hundreds of structural drawing sets over my 6 years in oil and gas. Equipment platforms, pipe racks, foundations - each project teaches you something new about how to catch problems before they become $50,000 change orders.&lt;/p&gt;

&lt;p&gt;Here's the workflow I use for every coordination review. This isn't theory. It's what works when you're responsible for making sure a 200-ton reactor platform doesn't fall over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Big Picture
&lt;/h2&gt;

&lt;p&gt;Before you look at a single beam detail, understand what you're building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read these documents first:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design basis memo&lt;/li&gt;
&lt;li&gt;Equipment data sheets&lt;/li&gt;
&lt;li&gt;Process flow diagrams&lt;/li&gt;
&lt;li&gt;Site survey&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need context. Is this a new structure or a retrofit? What loads are we dealing with? I once caught a foundation design that used 100 psf live load when the actual equipment imposed 400 psf. The architect had copied specs from an office building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Pass Review System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pass 1: Layout and Geometry
&lt;/h3&gt;

&lt;p&gt;Open the plan views. Check these items in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grid alignment&lt;/strong&gt; - Do structural grids match architectural grids?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elevation consistency&lt;/strong&gt; - Are finished floor elevations the same across all drawings?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equipment locations&lt;/strong&gt; - Do vessel centerlines match between P&amp;amp;ID and structural plans?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a simple checklist. I use a Python script that generates this for every project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;checklist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grid_alignment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elevation_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;equipment_centerlines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clearances_met&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You fill it out as you go. Binary answers only - yes or no.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 2: Member Sizing and Connections
&lt;/h3&gt;

&lt;p&gt;Now you get into the structural details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check these elements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beam depths against headroom requirements&lt;/li&gt;
&lt;li&gt;Connection details at equipment supports&lt;/li&gt;
&lt;li&gt;Foundation sizes vs equipment loads&lt;/li&gt;
&lt;li&gt;Seismic bracing locations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep a reference table of standard sizes we use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Equipment Type&lt;/th&gt;
&lt;th&gt;Typical Beam&lt;/th&gt;
&lt;th&gt;Min Foundation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Horizontal vessel&lt;/td&gt;
&lt;td&gt;W18x35 min&lt;/td&gt;
&lt;td&gt;8'x8'x3'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical pump&lt;/td&gt;
&lt;td&gt;W12x26 min&lt;/td&gt;
&lt;td&gt;6'x6'x2.5'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heat exchanger&lt;/td&gt;
&lt;td&gt;W21x44 min&lt;/td&gt;
&lt;td&gt;10'x6'x3'&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't code requirements. They're based on what actually works in the field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 3: Constructability Review
&lt;/h3&gt;

&lt;p&gt;This is where experience matters most. You're looking for things that look right on paper but won't work with a crane and actual workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flags I watch for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bolted connections you can't reach with a wrench&lt;/li&gt;
&lt;li&gt;Concrete pours that require impossible forming&lt;/li&gt;
&lt;li&gt;Steel members that interfere during erection sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned this the hard way on a platforming project in 2022. The drawing showed perfect bolt access. But the actual pipe routing made it impossible to get a socket wrench on 40% of the connections. We had to redesign three beam-to-column joints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation That Actually Helps
&lt;/h2&gt;

&lt;p&gt;Don't write novels in your review comments. Use this format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DWG: S-101
ITEM: Foundation F-3
ISSUE: Foundation 6'x6' but equipment data shows 8'x4' bolt pattern
ACTION: Resize foundation or confirm equipment dimensions
PRIORITY: HIGH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines maximum. If you can't explain the problem in four lines, you don't understand it well enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Coordination Failures
&lt;/h2&gt;

&lt;p&gt;These show up on 80% of the projects I review:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipe rack column spacing&lt;/strong&gt; - Process wants 30' bays, structural used 25' modules. Someone has to give.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Equipment access&lt;/strong&gt; - The structure works but maintenance can't remove the pump impeller without cutting steel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Utility routing&lt;/strong&gt; - Perfect structural design that leaves nowhere to run the 4" cooling water line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundation conflicts&lt;/strong&gt; - New foundation conflicts with existing underground utilities that weren't surveyed.&lt;/p&gt;

&lt;p&gt;I caught this last one three times in 2023. Each time saved at least two weeks of schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools That Speed Up Reviews
&lt;/h2&gt;

&lt;p&gt;I use RISA-3D for quick load path checks during review. If something looks questionable, I can model the critical members in 10 minutes and verify capacity.&lt;/p&gt;

&lt;p&gt;For large drawing sets, I wrote a Python script that extracts member sizes from PDFs and flags anything outside our standard size ranges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_beam_sizes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drawing_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;beam_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;W(\d+)x(\d+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;beams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beam_pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drawing_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;beams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;flag_undersized_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple automation that catches obvious problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Goal
&lt;/h2&gt;

&lt;p&gt;You're not just checking calculations. You're making sure someone can build this thing without calling you every day with problems.&lt;/p&gt;

&lt;p&gt;The best structural drawing review finds the issues that would stop construction. The worst one focuses on line weights and text fonts while missing the fact that the crane can't reach half the steel connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your next step:&lt;/strong&gt; Take your last drawing review and count how many comments were about constructability vs drafting standards. If it's less than 70% constructability, you're reviewing the wrong things.&lt;/p&gt;

&lt;p&gt;Focus on what breaks in the field, not what looks perfect on paper.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>career</category>
      <category>structural</category>
      <category>process</category>
    </item>
    <item>
      <title>How VAPI connects to Asterisk PJSIP for AI voice calls</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Wed, 20 May 2026 10:00:02 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/how-vapi-connects-to-asterisk-pjsip-for-ai-voice-calls-b6d</link>
      <guid>https://dev.to/domoniqueluchin/how-vapi-connects-to-asterisk-pjsip-for-ai-voice-calls-b6d</guid>
      <description>&lt;p&gt;I run six AI-powered businesses from a single Vultr VPS. Each business needs phone capabilities. Instead of paying $50+ per month for hosted voice solutions, I built my own system using Asterisk and VAPI.&lt;/p&gt;

&lt;p&gt;Here's exactly how I connected VAPI to Asterisk PJSIP for AI voice calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with SaaS Voice Solutions
&lt;/h2&gt;

&lt;p&gt;Most AI voice platforms want you to use their phone numbers and infrastructure. You pay per minute, per call, per feature. For one business, maybe that works. For six businesses making hundreds of calls monthly, those costs add up fast.&lt;/p&gt;

&lt;p&gt;I needed control. I needed my own phone system that could handle AI agents without monthly subscriptions bleeding my profit margins.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Setup Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt;: Vultr VPS (4GB RAM, 2 vCPU)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PBX&lt;/strong&gt;: Asterisk 18 with PJSIP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Voice&lt;/strong&gt;: VAPI agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIP Provider&lt;/strong&gt;: Flowroute (you can use any)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly Cost&lt;/strong&gt;: $47 total (server + DID numbers + minutes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup handles inbound and outbound AI calls for all six businesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asterisk PJSIP Configuration
&lt;/h2&gt;

&lt;p&gt;First, configure your PJSIP endpoints in &lt;code&gt;/etc/asterisk/pjsip.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[transport-udp]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;transport&lt;/span&gt;
&lt;span class="py"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;udp&lt;/span&gt;
&lt;span class="py"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0:5060&lt;/span&gt;

&lt;span class="nn"&gt;[vapi-endpoint]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;endpoint&lt;/span&gt;
&lt;span class="py"&gt;context&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;vapi-context&lt;/span&gt;
&lt;span class="py"&gt;disallow&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;
&lt;span class="py"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ulaw&lt;/span&gt;
&lt;span class="py"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;alaw&lt;/span&gt;
&lt;span class="py"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;vapi-auth&lt;/span&gt;
&lt;span class="py"&gt;aors&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;vapi-aor&lt;/span&gt;
&lt;span class="py"&gt;direct_media&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;ice_support&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;media_encryption&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sdes&lt;/span&gt;

&lt;span class="nn"&gt;[vapi-auth]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;auth&lt;/span&gt;
&lt;span class="py"&gt;auth_type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;userpass&lt;/span&gt;
&lt;span class="py"&gt;username&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;vapi-user&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-secure-password&lt;/span&gt;

&lt;span class="nn"&gt;[vapi-aor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;aor&lt;/span&gt;
&lt;span class="py"&gt;max_contacts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;remove_existing&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dialplan for AI Call Routing
&lt;/h2&gt;

&lt;p&gt;Your dialplan (&lt;code&gt;/etc/asterisk/extensions.conf&lt;/code&gt;) routes calls to VAPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[vapi-context]&lt;/span&gt;
&lt;span class="py"&gt;exten&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; _X.,1,NoOp(Incoming call for AI: ${CALLERID(num)})&lt;/span&gt;
&lt;span class="py"&gt;same&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; n,Set(CHANNEL(hangup_handler_wipe)=vapi-cleanup,s,1)&lt;/span&gt;
&lt;span class="py"&gt;same&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; n,Dial(PJSIP/vapi-endpoint,30)&lt;/span&gt;
&lt;span class="py"&gt;same&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; n,Hangup()&lt;/span&gt;

&lt;span class="nn"&gt;[vapi-cleanup]&lt;/span&gt;
&lt;span class="py"&gt;exten&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; s,1,NoOp(Call ended, cleanup complete)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For outbound calls through your SIP provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[outbound-context]&lt;/span&gt;
&lt;span class="py"&gt;exten&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; _1NXXNXXXXXX,1,NoOp(Outbound call to: ${EXTEN})&lt;/span&gt;
&lt;span class="py"&gt;same&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; n,Dial(PJSIP/${EXTEN}@flowroute-trunk,60)&lt;/span&gt;
&lt;span class="py"&gt;same&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; n,Hangup()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  VAPI Integration Code
&lt;/h2&gt;

&lt;p&gt;VAPI needs to register with your Asterisk server as a SIP client. Here's the Python code I use to manage the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_vapi_phone_number&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.vapi.ai/phone-number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;byom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;byomPhoneNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-asterisk-server.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vapi-user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-secure-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistantId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-assistant-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;VAPI_API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Configure your AI assistant
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_vapi_assistant&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.vapi.ai/assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eleven-labs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voiceId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-voice-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firstMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, how can I help you today?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;VAPI_API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connection Flow
&lt;/h2&gt;

&lt;p&gt;Here's how the pieces connect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inbound Call&lt;/strong&gt;: Customer dials your number → SIP provider → Asterisk → VAPI agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound Call&lt;/strong&gt;: VAPI agent → Asterisk → SIP provider → Customer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Processing&lt;/strong&gt;: Real-time audio flows between caller and AI through PJSIP channels&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configuration Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Audio Codec Issues&lt;/strong&gt;: VAPI works best with ulaw and alaw. Don't enable high-definition codecs unless you want choppy AI responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT Problems&lt;/strong&gt;: If your Asterisk server is behind NAT, add these lines to &lt;code&gt;pjsip.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[global]&lt;/span&gt;
&lt;span class="py"&gt;external_media_address&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-public-ip&lt;/span&gt;
&lt;span class="py"&gt;external_signaling_address&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-public-ip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firewall Rules&lt;/strong&gt;: Open UDP port 5060 for SIP signaling and UDP ports 10000-20000 for RTP audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Performance Numbers
&lt;/h2&gt;

&lt;p&gt;My setup handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12 concurrent AI calls without issues&lt;/li&gt;
&lt;li&gt;Average call setup time: 2.3 seconds&lt;/li&gt;
&lt;li&gt;Audio latency: 150-200ms (includes AI processing)&lt;/li&gt;
&lt;li&gt;Monthly costs: $47 for unlimited calls within my limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare that to hosted solutions charging $0.10+ per minute for AI calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;You control your phone infrastructure. No vendor lock-in. No surprise billing. No rate limits beyond your hardware capacity.&lt;/p&gt;

&lt;p&gt;When one of my businesses needs a new phone number or different call routing, I make the change in my dialplan. Takes 5 minutes. No support tickets or billing adjustments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Start with a basic Asterisk installation on a VPS. Get PJSIP working with a simple SIP provider first. Then add VAPI integration once your PBX handles regular calls properly.&lt;/p&gt;

&lt;p&gt;You'll spend a weekend learning Asterisk configuration. But you'll save hundreds monthly and own your communication stack completely.&lt;/p&gt;

&lt;p&gt;Want the complete configuration files? I've documented my entire setup at [your documentation link]. Stop paying monthly fees for infrastructure you can own.&lt;/p&gt;

</description>
      <category>voip</category>
      <category>ai</category>
      <category>selfhosted</category>
      <category>asterisk</category>
    </item>
    <item>
      <title>How I use RISA-3D to model equipment platform loads from a P&amp;ID</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Tue, 19 May 2026 10:00:06 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/how-i-use-risa-3d-to-model-equipment-platform-loads-from-a-pid-1m9m</link>
      <guid>https://dev.to/domoniqueluchin/how-i-use-risa-3d-to-model-equipment-platform-loads-from-a-pid-1m9m</guid>
      <description>&lt;h1&gt;
  
  
  How I use RISA-3D to model equipment platform loads from a P&amp;amp;ID
&lt;/h1&gt;

&lt;p&gt;P&amp;amp;IDs contain the structural loads you need. You just have to know how to extract them.&lt;/p&gt;

&lt;p&gt;After 6 years designing pipe racks and equipment platforms in oil &amp;amp; gas, I've learned that the most critical step happens before you even open RISA-3D. You need to decode the P&amp;amp;ID properly. Miss a pump or underestimate a vessel load, and your platform design fails.&lt;/p&gt;

&lt;p&gt;Here's my workflow for turning P&amp;amp;ID symbols into accurate structural models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create your equipment inventory
&lt;/h2&gt;

&lt;p&gt;I start with a spreadsheet. Every piece of equipment gets catalogued:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag Number&lt;/th&gt;
&lt;th&gt;Equipment Type&lt;/th&gt;
&lt;th&gt;Operating Weight (lbs)&lt;/th&gt;
&lt;th&gt;Dimensions (ft)&lt;/th&gt;
&lt;th&gt;Elevation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P-101A/B&lt;/td&gt;
&lt;td&gt;Centrifugal Pump&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;6 x 3 x 4&lt;/td&gt;
&lt;td&gt;El. 115'-0"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V-201&lt;/td&gt;
&lt;td&gt;Separator Vessel&lt;/td&gt;
&lt;td&gt;18,500&lt;/td&gt;
&lt;td&gt;12 dia x 25&lt;/td&gt;
&lt;td&gt;El. 120'-0"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-301&lt;/td&gt;
&lt;td&gt;Heat Exchanger&lt;/td&gt;
&lt;td&gt;8,200&lt;/td&gt;
&lt;td&gt;4 x 12 x 6&lt;/td&gt;
&lt;td&gt;El. 118'-0"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The P&amp;amp;ID shows you what's there. Equipment data sheets give you the weights. Don't guess these numbers. I've seen platforms fail because someone estimated a 15,000 lb vessel at 10,000 lbs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Identify load paths
&lt;/h2&gt;

&lt;p&gt;Equipment doesn't float. Every piece needs structural support. I trace each load path from equipment to foundation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct bearing&lt;/strong&gt;: Heavy vessels typically bear directly on steel beams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vibrating equipment&lt;/strong&gt;: Pumps and compressors need isolation springs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Piping loads&lt;/strong&gt;: Large bore piping creates significant lateral forces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance access&lt;/strong&gt;: Platforms need live load capacity for personnel and lifting equipment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your P&amp;amp;ID won't show you this. You need to visualize the 3D arrangement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set up the RISA-3D model
&lt;/h2&gt;

&lt;p&gt;I build my platform model in this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Create main structural grid
2. Define beam sections (W12x40 typical for equipment beams)
3. Add columns and bracing
4. Apply boundary conditions
5. Define load combinations per AISC 360
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For equipment platforms, I use these typical member sizes as starting points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Equipment beams&lt;/strong&gt;: W12x40 minimum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framing beams&lt;/strong&gt;: W10x26&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns&lt;/strong&gt;: W8x35 or HSS12x12x1/2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bracing&lt;/strong&gt;: HSS6x6x5/16&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Apply equipment loads
&lt;/h2&gt;

&lt;p&gt;This is where the P&amp;amp;ID analysis pays off. In RISA-3D, I model equipment loads as point loads with these considerations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static loads (Dead Load)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Equipment operating weight + piping + insulation + misc. steel
Factor: 1.2 (LRFD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dynamic loads (Live Load)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maintenance personnel: 125 psf
Equipment removal: 1.5 x equipment weight
Piping expansion forces: from stress analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the separator vessel example above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dead load: 18,500 lbs operating + 2,000 lbs piping = 20,500 lbs&lt;/li&gt;
&lt;li&gt;Live load: 27,750 lbs for removal case (1.5 x 18,500)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I apply these as nodal loads in RISA-3D at the actual equipment support points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Model piping restraint loads
&lt;/h2&gt;

&lt;p&gt;P&amp;amp;IDs show pipe routing but not restraint loads. You need to coordinate with the piping stress engineer. I typically see these magnitudes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchor loads&lt;/strong&gt;: 5,000 to 50,000 lbs depending on line size and pressure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide loads&lt;/strong&gt;: 1,000 to 10,000 lbs lateral&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring hanger loads&lt;/strong&gt;: Variable based on supported pipe weight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In RISA-3D, I model these as applied loads at beam locations where pipe supports attach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Check your work
&lt;/h2&gt;

&lt;p&gt;Before running analysis, I verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total platform dead load matches equipment inventory ± 10%&lt;/li&gt;
&lt;li&gt;Load paths are continuous to foundations&lt;/li&gt;
&lt;li&gt;All equipment has adequate support points&lt;/li&gt;
&lt;li&gt;Live load combinations include maintenance scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common mistakes I catch here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting piping loads (can be 30% of total load)&lt;/li&gt;
&lt;li&gt;Underestimating access platform requirements&lt;/li&gt;
&lt;li&gt;Missing thermal expansion effects&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Analyze and iterate
&lt;/h2&gt;

&lt;p&gt;RISA-3D gives you member utilization ratios. For equipment platforms, I target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beams&lt;/strong&gt;: 85% maximum utilization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns&lt;/strong&gt;: 80% maximum utilization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deflections&lt;/strong&gt;: L/240 for equipment beams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If members are overstressed, I resize and reanalyze. The P&amp;amp;ID constraints are fixed. Your structure must accommodate them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real project example
&lt;/h2&gt;

&lt;p&gt;Last month I designed a platform for three centrifugal pumps. The P&amp;amp;ID showed simple pump symbols. The reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each pump: 3,200 lbs + 1,800 lbs piping&lt;/li&gt;
&lt;li&gt;Suction/discharge forces: 8,500 lbs combined&lt;/li&gt;
&lt;li&gt;Maintenance crane load: 12,000 lbs&lt;/li&gt;
&lt;li&gt;Platform size: 20' x 30'&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total design load: 67,000 lbs on a platform that would carry 15,000 lbs if I'd only considered pump weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your next step
&lt;/h2&gt;

&lt;p&gt;Start with your current project P&amp;amp;ID. List every piece of equipment. Get the actual weights from data sheets, not estimates. Build your equipment inventory spreadsheet before you touch RISA-3D.&lt;/p&gt;

&lt;p&gt;The P&amp;amp;ID tells you what to design for. RISA-3D tells you if your design works. Master the translation between them, and your platforms will stand.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>structural</category>
      <category>software</category>
      <category>career</category>
    </item>
    <item>
      <title>Zero-downtime deployments on a single Vultr VPS with Docker and Nginx</title>
      <dc:creator>Domonique Luchin</dc:creator>
      <pubDate>Mon, 18 May 2026 10:00:04 +0000</pubDate>
      <link>https://dev.to/domoniqueluchin/zero-downtime-deployments-on-a-single-vultr-vps-with-docker-and-nginx-3if7</link>
      <guid>https://dev.to/domoniqueluchin/zero-downtime-deployments-on-a-single-vultr-vps-with-docker-and-nginx-3if7</guid>
      <description>&lt;p&gt;I run six AI-powered businesses from a single Vultr VPS. When I deploy updates to my VAPI agents or Supabase integrations, downtime costs me real money. Customers calling my AI phone systems expect 24/7 availability.&lt;/p&gt;

&lt;p&gt;Here's how I achieve zero-downtime deployments using Docker and Nginx on one $40/month server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Basic Deployments
&lt;/h2&gt;

&lt;p&gt;Most developers stop their container, pull new code, rebuild, and restart. This creates 30-60 seconds of downtime per deployment. For my Load Bearing Empire businesses, that's unacceptable.&lt;/p&gt;

&lt;p&gt;Your users hit 502 errors. Your monitoring tools send alerts. You lose credibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Zero-Downtime Setup
&lt;/h2&gt;

&lt;p&gt;I use Docker Compose with multiple container instances behind Nginx. During deployments, I spin up new containers before stopping old ones.&lt;/p&gt;

&lt;p&gt;Here's my production structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/apps/
├── business-app/
│   ├── docker-compose.yml
│   ├── nginx.conf
│   └── deploy.sh
└── nginx/
    └── sites-enabled/
        └── business-app.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Compose Configuration
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;docker-compose.yml&lt;/code&gt; runs two instances of each service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-blue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;business-app-blue&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3001:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;app-green&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;business-app-green&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3002:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I call this blue-green deployment. One container serves traffic while the other stays ready for updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx Load Balancer Setup
&lt;/h2&gt;

&lt;p&gt;Nginx distributes requests between containers. Here's my &lt;code&gt;/etc/nginx/sites-enabled/business-app.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;business_app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt; &lt;span class="s"&gt;weight=100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3002&lt;/span&gt; &lt;span class="s"&gt;weight=0&lt;/span&gt; &lt;span class="s"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://business_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;weight=0&lt;/code&gt; setting keeps the green container as backup. During deployments, I flip these weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deployment Script
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;deploy.sh&lt;/code&gt; script handles the entire zero-downtime process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;ACTIVE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"port":[0-9]*'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACTIVE_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3001"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app-green"&lt;/span&gt;
    &lt;span class="nv"&gt;DEPLOY_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3002"&lt;/span&gt;
    &lt;span class="nv"&gt;ACTIVE_WEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"weight=100"&lt;/span&gt;
    &lt;span class="nv"&gt;DEPLOY_WEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"weight=100"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app-blue"&lt;/span&gt;  
    &lt;span class="nv"&gt;DEPLOY_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3001"&lt;/span&gt;
    &lt;span class="nv"&gt;ACTIVE_WEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"weight=100"&lt;/span&gt;
    &lt;span class="nv"&gt;DEPLOY_WEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"weight=100"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deploying to &lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_CONTAINER&lt;/span&gt;&lt;span class="s2"&gt; on port &lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Pull latest code and rebuild&lt;/span&gt;
git pull origin main
docker-compose build &lt;span class="nv"&gt;$DEPLOY_CONTAINER&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$DEPLOY_CONTAINER&lt;/span&gt;

&lt;span class="c"&gt;# Wait for container health check&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;10
curl &lt;span class="nt"&gt;-f&lt;/span&gt; http://localhost:&lt;span class="nv"&gt;$DEPLOY_PORT&lt;/span&gt;/health &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# Update Nginx upstream weights&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/server localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_PORT&lt;/span&gt;&lt;span class="s2"&gt; weight=[0-9]*/server localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_PORT&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_WEIGHT&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; /etc/nginx/sites-enabled/business-app.conf

&lt;span class="c"&gt;# Reload Nginx (zero downtime)&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload

&lt;span class="c"&gt;# Wait 30 seconds for connections to drain&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;30

&lt;span class="c"&gt;# Set old container as backup&lt;/span&gt;
&lt;span class="nv"&gt;OLD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;3003&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$DEPLOY_PORT&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/server localhost:&lt;/span&gt;&lt;span class="nv"&gt;$OLD_PORT&lt;/span&gt;&lt;span class="s2"&gt; weight=[0-9]*/server localhost:&lt;/span&gt;&lt;span class="nv"&gt;$OLD_PORT&lt;/span&gt;&lt;span class="s2"&gt; weight=0 backup/"&lt;/span&gt; /etc/nginx/sites-enabled/business-app.conf

nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deployment complete"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Health Checks Are Critical
&lt;/h2&gt;

&lt;p&gt;Your application needs a &lt;code&gt;/health&lt;/code&gt; endpoint. Mine returns the container port and timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment script uses this endpoint to verify the new container works before switching traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Migration Strategy
&lt;/h2&gt;

&lt;p&gt;For database changes, I run migrations before deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run migrations first&lt;/span&gt;
npm run migrate:up

&lt;span class="c"&gt;# Then deploy application&lt;/span&gt;
./deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because I design backward-compatible migrations. New columns get default values. Old columns stay until the next deployment cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Your Deployments
&lt;/h2&gt;

&lt;p&gt;I monitor deployment success with simple curl commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check both containers respond&lt;/span&gt;
curl &lt;span class="nt"&gt;-f&lt;/span&gt; http://localhost:3001/health
curl &lt;span class="nt"&gt;-f&lt;/span&gt; http://localhost:3002/health

&lt;span class="c"&gt;# Monitor response times&lt;/span&gt;
curl &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"@curl-format.txt"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null http://yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;curl-format.txt&lt;/code&gt; shows timing breakdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;time_namelookup&lt;/span&gt;:  %{&lt;span class="n"&gt;time_namelookup&lt;/span&gt;}\&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;time_connect&lt;/span&gt;:     %{&lt;span class="n"&gt;time_connect&lt;/span&gt;}\&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;time_appconnect&lt;/span&gt;:  %{&lt;span class="n"&gt;time_appconnect&lt;/span&gt;}\&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;time_total&lt;/span&gt;:       %{&lt;span class="n"&gt;time_total&lt;/span&gt;}\&lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resource Usage Reality Check
&lt;/h2&gt;

&lt;p&gt;This setup uses about 2GB RAM for two Node.js containers plus Nginx on my 4GB Vultr VPS. CPU usage stays under 20% during deployments.&lt;/p&gt;

&lt;p&gt;Your mileage varies based on application size and deployment frequency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Beats Kubernetes
&lt;/h2&gt;

&lt;p&gt;Kubernetes offers similar capabilities but requires 3+ nodes for true high availability. That's $120+ monthly versus my $40 VPS.&lt;/p&gt;

&lt;p&gt;For small businesses, this Docker approach provides 99.9% uptime without the complexity tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Next Steps
&lt;/h2&gt;

&lt;p&gt;Start with health checks in your existing application. Add the &lt;code&gt;/health&lt;/code&gt; endpoint this week. Set up the blue-green containers next weekend. Deploy the Nginx configuration after testing locally.&lt;/p&gt;

&lt;p&gt;You'll sleep better knowing your deployments don't wake up angry customers.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
