Operating a Production SaaS with Claude as the Only Engineer
Over 170 days live. 1,683 bookings processed. 1,067 unique guests served. 1,595 stays completed. Two paying tenants. Zero lines of code written unassisted.
I’m not a software engineer. I have never written a Laravel controller from scratch. I cannot solve a LeetCode medium without help. I would fail your take-home test.
I have also been running a multi-tenant hotel property management system in production for over 170 days, with real boutique hotels paying me to use it, on Postgres on a single 16-core Hetzner box running k3s. I am the only person on the project. Every line of code in production was written by Claude. Every line of code that runs in production was shipped by me.
This is what I’ve learned about how that actually works, and where the limits are.
The product
FluxPMS is a multi-tenant property management system for boutique hotels. The lead tenant, Harvest Hotel Retalhuleu, has been on the system since November 2025. As of writing:
- Over 170 days in production (live tenant since Nov 29, 2025)
- 1,683 bookings processed
- 1,067 unique guests served
- 1,595 stays completed end-to-end (check-in through check-out)
- 2 paying tenants in production (Harvest Hotel Retalhuleu + Reforma), Guatemalan sales motion active
The stack:
- Laravel 12 on FrankenPHP / Octane
- PostgreSQL 17
- Redis (sessions, cache, queues)
- MinIO (S3-compatible object storage)
- k3s on Hetzner — one node, 16 cores, 62 GB RAM
- Cloudflare in front of the marketing surface, DNS-only for tenant subdomains
- A separate Tauri 2 desktop app and a sister POS product, both Drizzle/SQLite locally with the same schema as the cloud
I do not write the code. I orchestrate Claude through a workflow I built and refused to merge against, day after day, for 18 months.
The verify loop
The single most important thing I learned is that “the model wrote the code” is not “the code is shipped.” Closing the gap between those two states is the whole job.
My verify loop on every change, for the PHP / Blade / Livewire surface:
1. Read the file with the Read tool (not cat, not a multi-line regex).
2. Edit with the Edit tool — exact match, one concern per edit.
3. Grep every consumer of the changed file / symbol:
grep -rIE "<filename>" app/ resources/ routes/
grep -rIE "<class or function>" app/
4. Push to the pod (kubectl cp); verify with head -3 that the
deployed file is what I intended to deploy.
5. php artisan view:clear && cache:clear && config:clear && optimize:clear
6. php artisan octane:reload ← non-negotiable; without it, old bytecode wins
7. php artisan test --filter=<relevant>
8. curl -sS the user-facing https URL with a valid auth cookie:
curl -sS -c $J -b $J -o /tmp/o.html -w '%{http_code}' \
https://demo-hotel.dev.fluxpms.com/<route>
head -200 /tmp/o.html | grep <expected-string>
9. Only after step 8 returns the expected string is the change marked done.This is mostly the boring stuff. It’s also the difference between “the model says it shipped” and “the user can actually use the feature.” If you skip step 6 in this stack, the file is in the pod but the old PHP bytecode still serves the request and the user sees stale behavior for 90 seconds. If you skip step 8, you’re trusting the model’s self-report instead of the user’s actual experience.
I’m convinced that most “I built X with AI” failure stories — including a few of my own — come from doing the first half of that loop and skipping the second half.
The operating rules
I keep an explicit operating-rules file the model loads every session. It’s about 200 lines. Some of the rules:
- Forward only. No rollbacks. No
git revert. Nogit reset --hard. When a change regresses something, debug the failure and patch forward. The only exception is when the entire codebase is unrunnable AND a known-good backup exists. - Never claim done without end-to-end proof. A change is not shipped until the verify loop completes. File presence in the pod is not shipped. “Should be live now” is not shipped.
- No patches around problems. No
--legacy-peer-deps. No// @ts-ignore. No catch-and-swallow. No hardcoded “for now” values. Identify the root cause, fix at the source, pin clean versions. - Never touch the prod namespace without explicit approval. The lead tenant runs on a separate namespace. Read-only by default. I have lost it multiple times in earlier sessions and the rule exists because of those incidents.
- No hedging. Banned words in the model’s output to me: “honest,” “honestly,” “frankly,” “however / that said.” State the fact directly.
The rules are not aspirational. The model follows them because I refuse to merge anything that breaks them. The few times I tried to be lenient — “just this once” — I produced incidents. The rules are the reason I can run this product alone.
What the model is good at
- Designing internally consistent multi-tenant schemas faster than I could.
- Writing Eloquent queries with eager loading that don’t N+1.
- Refactoring across many files when the change is mechanical.
- Pattern-matching production failures from log snippets I paste in.
- Producing complete deployments — Dockerfiles, k8s manifests, ingress configs — that work the first or second time.
What the model is bad at
This is where the founder’s job lives.
- Deciding what to build. The model will happily build the thing you ask for. The thing you should be building is often different. The verify loop catches functional errors; it does not catch “we built the wrong feature.”
- Knowing when something is shipped. The model will say “deployed and should be live now” while staring at a 502 page. Until step 8 grep-matches the expected response, nothing is shipped. The model does not enforce this on itself; I have to.
- Resisting the urge to patch around root causes. When something breaks, the model’s first instinct is often to suppress the symptom. “Just catch this exception.” “Just ignore this case.” “Just add a feature flag.” Letting that happen is how a codebase gets unmaintainable. Stopping it is enforcement work, not coding work.
- Knowing the customer. I built FluxPMS the shape it is because I worked behind a hospitality counter in Antigua Guatemala in 2018. The model can’t replace that operator background. It can only build to it.
What “shipping” actually looks like
A representative session: a tenant reports that voided payments are still being counted in the revenue dashboard. The work to fix it:
- Read
Dashboard.php,Today.php,Payments.php, the AnalyticsService and AriaService. - Grep every consumer of
Payment::where(...). Find seven places. Decide which need theis_void = falsefilter. - Decide the new canonical filter shape: a 4-status check with
is_void = false. - Make the edit in all seven files.
- Run
kubectl cpto each affected pod.head -3to verify the deployment. - view:clear, cache:clear, config:clear, optimize:clear, octane:reload.
- curl -sS the dashboard with the tenant’s auth cookie, grep for the expected number.
- Confirm with the tenant in WhatsApp that the dashboard shows the corrected total.
- Save a memory note in the model’s session-persistent memory so the next session inherits the canonical metric blueprint.
That’s about 90 minutes of work for an issue that, when I started, would have taken me four days of writing Laravel I couldn’t actually write. The 90 minutes is mostly running the verify loop and reading the diff. The model writes the code. I refuse to merge code that hasn’t been verified.
What this is not
This is not “vibe coding.” Vibe coding is what produces incidents. What I’m describing is the opposite of vibe coding — it’s structured orchestration with an enforced verify loop on top of a model that’s doing the typing.
This is also not “Claude does the engineering for me.” Claude can type faster than me and is better than me at boilerplate. It cannot decide what to build, when to ship, or what failure modes a hotel front-desk operator will hit at 11pm on a Saturday with three angry guests in line. Those are the parts of software engineering that aren’t typing, and those are the parts I do.
What I’m looking for
This is the kind of work I want to do, full-time, for an organization that takes AI-in-production seriously. Forward Deployed Engineering, Solutions Engineering, Customer / Applied AI roles — the seat where the job is take a real customer problem to a production solution with the model in the loop, end-to-end.
I’m based in Quetzaltenango, Guatemala. I’m willing to relocate.
If your team hires for that kind of role and any of this sounds useful, my email is [email protected] and the production proof is live at hotelesharvest.com.