Since January 2026 I have been the only IT person at H2O Professional Services, a plumbing company in Richmond with an office staff and a crew of field techs. This is what I built, why I built it that way, and the two failures that taught me the most.
When I started, the company ran on a field-service CRM, a handful of cloud accounts, personal phones, and passwords people remembered. Nothing was wrong enough to stop work, which is exactly why nobody had fixed it. I was also working full time on an enterprise help desk, so everything here had to run without me watching it.
My rule for the whole project: build it so the business owns it, so it tells me when it breaks, and so the next person can read how it works.
I set up Microsoft 365 and Intune for the whole fleet: enrollment, configuration profiles, compliance policies, and conditional access, so a device that falls out of compliance stops getting company data until it is fixed. Phones that ride around in trucks can be locked or wiped remotely.
Staff also needed apps without filing a ticket and waiting on me. I built a small request page: someone asks for an app, I approve it, and it installs through Intune on its own. Each app gets its own assignment group, so approving one person never changes anyone else's devices.
With one IT person, anything that needs me in the loop becomes a queue. Compliance rules and self-service turn "ask Connor" into "it just happens," and I only step in to approve.
One lesson from this part: in Intune's API, assigning an app replaces the existing assignments instead of adding to them. I learned it when an update quietly removed an app from a group of devices. Now every change reads the current assignments first and writes back the full set.
Inventory, reporting, dispatch tools, and the company's internal dashboards run in Docker containers on a small in-office server. A Caddy reverse proxy sits in front and handles certificates automatically. Every internal tool is behind Google single sign-on, so there is one account to turn off when someone leaves.
Uptime monitoring checks each service and alerts me before anyone calls. The configuration lives in a Git repository that mirrors to a private remote, and secrets stay out of it, in environment files only.
Per-seat SaaS for every small need adds up fast for a company this size, and it scatters accounts everywhere. One box, one sign-in, and one place to look when something breaks is easier for one person to keep healthy.
The company moved its field-service work from ServiceTitan to Jobber. I wrote Python scripts against both APIs to carry the history across: customers, jobs, visits, invoices, the price book, and tens of thousands of attached photos and documents. After the move, scheduled jobs keep new invoices and paid balances in step between the two systems while the office finishes the transition.
An import tool gets most of the records over. The last few percent (who sold the job, which tech did the work, what was already paid) is what payroll and bonuses depend on, and it only comes over if you check it record by record.
Rules I held to because the data is money:
The main business number moved from ServiceTitan's phone system to CallRail. A week later, the weekly after-hours report said the on-call phone was having a slow week. It wasn't. It was reporting about half the real calls.
I was monitoring whether the jobs ran, not whether the data was real. A job that succeeds with zero rows is not a healthy job.
The nightly sync that copies ServiceTitan data into the company's reporting database stopped moving forward for about a week. Nothing alerted. The dashboards just kept showing week-old numbers.
The root cause was a change to a different script. Rewriting one reporting module had broken a sign-in helper that the sync borrowed from it. The sync failed early, and it failed quietly.
I fixed the shared helper, let the sync catch up, and then reconciled one customer's account line by line against the source to prove the data was right again, instead of assuming it.
Shared code is shared risk. Anything more than one job depends on belongs in its own module with its own check, and freshness ("when did this table last change?") is worth alerting on by itself.
Both failures came from things that "ran fine." The checks I care about now are counts and timestamps compared to something independent.
Dry runs by default, re-reads before writes, and no automated money movement. Slower to build, and it has never cost me a bad write.
Every system here has notes on how it works and what bit me. That is what makes a one-person setup survivable, including for whoever comes after me.
Happy to walk through any part of it in more detail.