<?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: Reza Jodeiri</title>
    <description>The latest articles on DEV Community by Reza Jodeiri (@reza_jodeiri_7c4bf61ba5e8).</description>
    <link>https://dev.to/reza_jodeiri_7c4bf61ba5e8</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%2F4187145%2F2efca9a8-c43c-48e1-a5d1-c81f050bc399.jpg</url>
      <title>DEV Community: Reza Jodeiri</title>
      <link>https://dev.to/reza_jodeiri_7c4bf61ba5e8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reza_jodeiri_7c4bf61ba5e8"/>
    <language>en</language>
    <item>
      <title>I run a drainage company. I got sick of paying for CRM software, so I built my own. Meet Relayhand.</title>
      <dc:creator>Reza Jodeiri</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:51:23 +0000</pubDate>
      <link>https://dev.to/reza_jodeiri_7c4bf61ba5e8/i-run-a-drainage-company-i-got-sick-of-paying-for-crm-software-so-i-built-my-own-meet-relayhand-1n85</link>
      <guid>https://dev.to/reza_jodeiri_7c4bf61ba5e8/i-run-a-drainage-company-i-got-sick-of-paying-for-crm-software-so-i-built-my-own-meet-relayhand-1n85</guid>
      <description>&lt;p&gt;If you've ever hired someone to fix a drain, replace an AC unit or regrade a yard, you've seen the back office of a small trade business. The quote was a photo of a notepad, or a PDF made from a Word template. The contract was the same PDF with "sign here" typed at the bottom.&lt;/p&gt;

&lt;p&gt;I don't just know this world; I live in it. I own and operate a residential drainage company in Memphis, Tennessee. The thing that bothered me most about this industry wasn't how messy the back office looked. It was how much money leaked out of it. A lead that goes quiet for four days usually stays quiet. A contract signed on paper can't be found when there's a dispute. And a deposit nobody asked for at signing is a deposit that never comes in.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Relayhand&lt;/strong&gt;. It's a CRM for small trade contractors: one owner working alone, or a few crews with someone in the office. Today I'm opening it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Relayhand covers the stretch of work between "someone called about a job" and "the job is paid for." It's built around one loop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A lead comes in.&lt;/strong&gt; It can come from a call, a text or the contractor's own website. Whatever the source, it lands in one place with the address and history attached, and it goes into a pipeline. The contractor defines the pipeline stages, since an HVAC company and a landscaper don't sell the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You send a quote.&lt;/strong&gt; Quotes are built from the contractor's own price catalog: measures (linear feet, square feet, units), options the customer chooses between, and line items priced from those. A customer can pick "good," "better" or "best," or combine add-ons. The quote goes out as a branded PDF with the contractor's logo and prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The customer signs on their phone.&lt;/strong&gt; They tap a link and sign from the driveway. This was the part I cared about most, and I'll get to why in the architecture section. Once a contract is sent, the document is frozen. What the customer signs is exactly what they saw, and nothing can change under them afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. You get paid.&lt;/strong&gt; The customer can put down a card deposit when they sign. When the job is finished, the balance is charged against the card already on file. The invoice always goes out before the charge. Both payments are recorded in a way that matches what the customer agreed to.&lt;/p&gt;

&lt;p&gt;Several other pieces sit around that loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up that actually happens.&lt;/strong&gt; When a lead goes quiet, Relayhand drafts a text for you. The draft waits in a queue for you to approve. Nothing is ever sent to a customer automatically. I made that a hard rule, because a contractor's reputation is local and one wrong message costs more than ten missed ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A schedule that works like a dispatch board.&lt;/strong&gt; Crews are rows and days are columns. You drag a visit to move it. There's a separate field app for the person driving to the house, and it keeps working without signal and syncs when the truck gets back into coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring service plans.&lt;/strong&gt; Maintenance contracts (quarterly HVAC checks, lawn care, pest control) are signed once, and every completed visit then gets invoiced and billed on the terms in that agreement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job costing and time tracking&lt;/strong&gt;, so the owner can see what each job actually earned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP server&lt;/strong&gt;, so an assistant like Claude can work with the CRM. It can look up a customer, draft a reply or pull a report. Anything that moves money takes two steps: the assistant previews the action, a person approves it, and only then does it run. Every money action is written to an append-only log.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;This is dev.to, so here's the part you probably came for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; React 18, TypeScript and Vite on the front end. Vercel serverless functions for the API. Supabase (Postgres) for the database and auth. Stripe Connect for payments. Vitest for tests. I build it alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy is enforced in Postgres, not in route code.&lt;/strong&gt; Every table has row-level security, in three tiers. Everyday records like customers, quotes and jobs can be written by any member of the business. Money tables like payments and stored-card mandates can only be written by an owner or admin. Logs like the email log and the AI action log can't be written by any user at all; only the server writes them. The reason is that Supabase's REST endpoint is reachable directly with the public key and a valid login token. A role check in an API route that isn't backed by a policy in the database isn't really a check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contracts are immutable in four layers.&lt;/strong&gt; This is where most of the design effort went:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A published contract template is frozen by a database trigger. Editing it creates a new version.&lt;/li&gt;
&lt;li&gt;A quote pins the exact template version when it's sent.&lt;/li&gt;
&lt;li&gt;The rendered HTML is stored at send time, and the signing page serves that snapshot. It never re-renders.&lt;/li&gt;
&lt;li&gt;A second snapshot is stored at the moment of signature, and the signed PDF is archived with a hash that a nightly job re-checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Changes to a signed contract don't edit it. They go through a separate amendment document that names the contract it changes. I also keep byte-exact snapshot tests of the rendered contracts, because a diff in a legal document should never slip through as a "small refactor."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payments are direct charges on the contractor's own Stripe account.&lt;/strong&gt; Relayhand doesn't hold anyone's money. Every Stripe call that touches a contractor's money passes their connected account ID, and every one-shot charge carries an idempotency key, so a retry can't double-charge a customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two bugs that taught me something
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;.js&lt;/code&gt; extension that took production down.&lt;/strong&gt; Some of my TypeScript modules (the contract renderer, for example) are shared between the browser and the Node serverless functions. Vite resolves &lt;code&gt;import { x } from './contractTokens'&lt;/code&gt; without complaint. Node's ESM resolver doesn't: it wants &lt;code&gt;'./contractTokens.js'&lt;/code&gt;, even though the file on disk is &lt;code&gt;.ts&lt;/code&gt;. I shipped one import without the extension. Typecheck passed, lint passed, the build passed, the tests passed. The contract view and signing endpoints then failed at module load with &lt;code&gt;FUNCTION_INVOCATION_FAILED&lt;/code&gt; before any handler ran.&lt;/p&gt;

&lt;p&gt;The fix took one line. The lesson was that my checks all ran in the wrong environment. Now there's a test that walks the import graph reachable from &lt;code&gt;api/&lt;/code&gt; and fails on any relative import without &lt;code&gt;.js&lt;/code&gt;, plus a test that actually loads every API module in Node. After every deploy I curl the endpoints too. A 401 means the function loaded; a 500 means it didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Six of my seven cron jobs never ran.&lt;/strong&gt; Vercel Cron calls your endpoint with &lt;code&gt;GET&lt;/code&gt; plus a bearer secret. Most of my cron handlers began with &lt;code&gt;if (req.method !== 'POST') return 405&lt;/code&gt;, a reasonable-looking guard I'd copied from my write routes. Locally I triggered them with POST, so they worked. In production every scheduled run was refused politely, and nothing alerted me, because a 405 isn't an error from Vercel's side. Now all crons go through one shared method check, and a test reads &lt;code&gt;vercel.json&lt;/code&gt; and checks every scheduled path.&lt;/p&gt;

&lt;p&gt;Both bugs had the same root cause: a guard that passes in the environment you test in tells you nothing about the one you deploy to. I now treat "what would this check &lt;em&gt;not&lt;/em&gt; see?" as part of writing the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Flat monthly prices per business, not per seat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo&lt;/strong&gt;: $49/month, 1 person&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crew&lt;/strong&gt;: $149/month, up to 5 people, adds scheduling, job costing, time tracking and service plans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shop&lt;/strong&gt;: $299/month, up to 15 people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no annual plan and no "contact sales" tier. Every plan has a 14-day free trial.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm going to be honest about where things stand. Relayhand is early. The core loop (lead, quote, signed contract, deposit, schedule, invoice) is what I've put the most work into. Some integrations are built but haven't been tested end to end against their live providers yet, and I won't advertise them until I've seen them work. If that sounds overly cautious for a launch post, it's on purpose: a contractor who pays for something because a marketing page said it works deserves for that to be true.&lt;/p&gt;

&lt;p&gt;If you run a trade business, or you're related to someone who does (everyone seems to be), I'd love for you to try it and tell me where it falls short: &lt;strong&gt;&lt;a href="https://relayhand.com" rel="noopener noreferrer"&gt;relayhand.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're a developer, I'm happy to go deeper on any of this in the comments. That includes the RLS setup, the contract snapshot design, two-step confirmation for AI tools that move money, or how I test serverless code that my unit tests can't reach.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
