I thought my English wasn't good enough. It was Claude's defaults.
2026-05-18
Customer Journeys Belong Next to Your Code
2026-05-13
2026-05-18
2026-05-13
I got beta access to FastAPI Cloud, the new hosting provider from the FastAPI team. This is a short report of what I deployed and how it went. It is not a full review.
The project I used is a hobby test project. Nothing critical. If it is unavailable I do not care. That made it a good fit for trying a beta hosting provider.
For context: my usual way to deploy a Python app is Docker on a VPS or a dedicated server. I have used Digital Ocean App Platform in the past. The developer experience was great, but I stopped using it because the price was not worth it for my use case. I have not tried Render or other managed Python hosting providers. So this is not a full comparison against managed hosting in general. It is just my experience trying FastAPI Cloud.
I part of the django app behind abelcastro.dev is a small API fetching and serving sports (actually just football) data. To try FastAPI Cloud, I extracted that part into a standalone FastAPI app. The code is here: github.com/abel-castro/sports-api and the live version of it can be found here sports-dashboard.abelcastro.dev/.
The migration itself was easy with AI help. The original code was simple, and the move to FastAPI plus SQLModel/SQLAlchemy was straightforward.
In his blog post about starting FastAPI Cloud, Sebastian Ramirez names Vercel, Netlify, and Cloudflare in the frontend world as the kind of developer experience he wanted to bring to Python deployment. Vercel was also the comparison I had in mind when I tried it.
Login and creating a first app were straightforward. It is not as seamless as Vercel today, but for a beta the starting experience is impressive.
One thing I liked is the direct integration with database providers. I connected the app to a Neon Postgres database from inside the FastAPI Cloud setup, and that worked well.
After the app was live, I noticed intermittent database connection errors. This was not a FastAPI Cloud problem. It is how Neon works. Neon suspends idle compute, so connections in the SQLAlchemy pool can become stale and fail on the next request.
The fix was to pass two extra options to create_engine:
engine = create_engine(
settings.database_url,
pool_pre_ping=True,
pool_recycle=1800,
)
pool_pre_ping=True makes SQLAlchemy check that a connection is alive before using it. pool_recycle=1800 tells the pool to discard connections older than 30 minutes. After this change the errors stopped.
The commit is here if you want to see it: Fix db connection errors.
The sports API needs a periodic job to refresh data. As far as I can tell, FastAPI Cloud does not support scheduled tasks at the moment.
For this hobby project I used a workaround. A scheduled GitHub Actions workflow calls a protected endpoint on the API. The endpoint checks a token passed in a header, and the token is stored as a GitHub secret. The workflow file and the endpoint are in the same repo linked above.
I want to be clear: this is a hobby pattern, not how I would do it for a real project. GitHub schedules can be delayed under load, there are no built-in retries, long jobs do not fit inside an HTTP request, and observability is split between two platforms. For a real project I would use a task queue with a worker (Celery, RQ, ARQ, Dramatiq) or a managed scheduler.
So my open question for the FastAPI Cloud team is: are proper scheduled tasks on the roadmap?
Right now you get an auto-assigned subdomain on FastAPI Cloud and there is no custom domain option yet. For a beta this is fine, and I would not complain about it. It is just something to know.
For a beta, FastAPI Cloud is impressive. The login and first deploy are easy, and the Neon integration works well (with the small SQLAlchemy tweak above). The one real gap I found was scheduled tasks.
I am looking forward to seeing how the product evolves, and what pricing they introduce. Hosting costs money, and at some point the team has to make money from it. In any case, in my opinion they are making a great start.
Last week I asked Claude to summarize a deploy log. It told me a job had "stalled". I spent a few minutes checking the system for actual problems before realizing the word was figurative. The job had paused, not broken. Stalled is what cars do.
This is the pattern. Every few prompts, Claude reaches for a phrase that sounds natural to a fluent reader and lands wrong on me. Drain, in-flight, beat us to it, head down, spin up. Each one is a small interruption. I read it twice, I check if it means what I think, sometimes I look it up. Multiply by 50 prompts a day.
For a long time I treated this as my problem. The author of the message is a tool I am paying for, and yet I was the one stumbling. It felt like a gap in my English, something I should fix by reading more.
It is not that. Claude's default writing is polished English: fluent, idiomatic, the way "good writing" is supposed to sound. For a non-native reader, that polish is the problem.
Once you frame it that way, the fix is a configuration issue, not a vocabulary problem.
Correcting Claude inside the conversation. It works for two or three turns and then the next idiom shows up. The corrections do not carry across sessions, and they do not carry between projects.
Maintaining a list of forbidden words. I tried this. The list grew. Drain. In-flight. Stalled. Beat us to it. Spin up. Tear down. English has thousands of these phrases. I cannot enumerate them, and I should not have to.
Both approaches fail for the same reason. They treat each idiom as a separate item to memorize, when the underlying issue is a writing principle the model is not applying.
The principle is one question, applied to every sentence the model writes:
Would this phrase still mean what I intended if translated word for word into another language?
If yes, great. If no, it is an idiom. You can usually replace it with clearer words.
A few examples:
The right column reads slightly less fluent and is much easier to understand. For a non-native reader this is not a downgrade, it is the entire point.
Once you have the principle, you do not need the list. The principle generates the list on demand.
Claude has a settings field called "Instructions for Claude", under Settings, General, Profile. Text in that box applies to every chat across the account. It is the right place for a writing rule that should always apply.
Paste this:
Write all prose, code comments, log messages, and chat responses
with literal vocabulary. The reader is a non-native English speaker.
Test before sending: would each phrase still mean what I intended
if translated word for word into another language? If not, rewrite it.
Avoid:
- Phrasal verbs whose meaning differs from the literal words
(spin up, take off, wind down, tear down, look up).
- Sports, war, cooking, or sailing metaphors (in the trenches,
simmer, on the hunt, beat us to it).
- Body-part metaphors (head down to, keep an eye on, lend a hand).
- Verbs that physically describe something else (stalled, drained,
burned down). Use neutral verbs that describe what is actually
happening (did not finish, stopped before completing).
- Niche jargon when a plain phrase is the same length (terminal
status, in flight). Prefer the plain phrase.
Keep technical terms with literal meanings (SIGTERM, graceful
shutdown, cache miss). The rule is about figurative language,
not standard terminology.
Keep sentences short and direct. Prefer the phrasing a 10-year-old
or a non-native English speaker would understand. Boring prose is
correct prose.
Save. From the next chat onward, Claude applies the rule by default.
The settings field above applies to claude.ai on the web, the desktop app, and the mobile app. These are different clients to the same backend, so one setting covers all three.
Claude Code (the CLI, and the VS Code extension that uses it) is a separate product with its own configuration. The web setting does not apply there. To get the same rule, paste the same text into a user-level file:
~/.claude/CLAUDE.md
Claude Code reads that file at the start of every session, in every project. Project-level CLAUDE.md files at the repo root also work, but for a writing rule that should always apply, the user-level file is the right place.
You now have two copies of the same text, one in the web settings and one in ~/.claude/CLAUDE.md. If you change one, update the other. There is no automatic sync between them.
The same principle works for anything I write that other developers might read. Documentation, commit messages, code review comments, Slack threads. An idiom in a PR description costs everyone two seconds and costs a non-native reader a search.
The literal-translation test is a habit I can apply with or without an LLM. The Claude instruction just makes the habit show up in every chat I have, without me having to remember.
I am building TheBest.Ink alone. The whole product vision lives in my head. Even with that, I was getting overwhelmed holding all the scenarios in mind. Who claims what. Which path triggers moderation. When an artist gets marked as listed.
So I opened docs/customer-journeys.md and started writing user journeys as prose, with a Mermaid flowchart for each one. The first time I rendered them, I had a small, sharp moment: "oh, that is what this project does." The charts gave the project a shape I could not see from the code alone. It felt like seeing the project's soul. Not in a mystical way, but as the actual sequence of promises the product makes to users.
This post is about that moment, and the artifact it produced. It follows two earlier posts of mine, one arguing that spec-driven development solves the wrong problem, the other arguing that human and agent docs should be the same docs. This is the doc category neither of those named.
I do not think every customer journey in every company must live next to the code. That would be too broad. Some journeys belong in product discovery, design tools, analytics dashboards, or support processes. But for journeys that define how the system should behave end-to-end, especially in a small product team or an AI-assisted codebase, I think there is a strong case for keeping them in the repo.
The file sits at docs/customer-journeys.md, one section per journey. Each section is a few paragraphs of prose, a Mermaid flowchart, and status markers (NYI for "not yet implemented", NI for "needs improvement", CTA for "call to action", NC for "needs clarification").
The artist discovery and booking flow looks like this:
Five journeys live in that file today: artist discovery, studio discovery, review, claiming, and new artist or studio creation. Each one starts with "what is the user trying to do here, end-to-end" and ends with the path through the system that gets them there.
A simplified journey section could look like this:
### J-ARTIST-CLAIM-01: Artist claims an existing profile
Status: NI
Intent:
An artist should be able to prove ownership of an existing profile without blocking legitimate claims too early.
Key guarantees:
- Unclaimed profiles show a claim CTA.
- Suspicious claims are not hard-blocked immediately.
- Claims with weak evidence enter moderation.
- Verified claims unlock profile editing.
Coverage:
- E2E: artist-claim.spec.ts
- Integration: claim-moderation.service.spec.ts
That format is still experimental, but the idea is important: the journey should not only describe a flow. It should also describe the product intent and the behavioral guarantees that matter.
The doc types most repos have do not cover this.
None of them answer "what is the user actually trying to accomplish, end-to-end, across modules?" That is the gap.
I want to be honest about the prior art, because there is a lot of it. User journey mapping has been a UX practice for years. Use cases go back to Ivar Jacobson. User story mapping comes from Jeff Patton. BDD and specification by example come from Dan North and Gojko Adzic. Domain storytelling is from Stefan Hofer and Henning Schwentner. Living documentation is from Cyrille Martraire. The building blocks are old.
What I am doing sits closest to domain storytelling plus living documentation plus BDD without the Gherkin syntax. The difference is small but real. None of the prior art was written with AI agents as a first-class reader. The shape of the artifact changes when the reader is not only a human PM or a developer on the team, but also an agent that will read three or five files in a cold-start context and try to do something useful.
That is the only thing I claim is new here. The format, the in-repo location, and the freshness mechanism follow from that one shift in audience.
When I think about what agents struggle with on my repo, three things come up over and over.
Cold-start orientation. An agent opens three to five files at the start of a task and needs to know what the project does. A top-level README gives it the elevator pitch. A module README gives it local scope. Neither tells it what the user is actually trying to accomplish across modules. The journey doc does. On TheBest.Ink, the difference between "search for artists" and "search for artists, view a card, fall back to an external channel, or follow the booking path" is the difference between a code tour and a usable orientation.
Cross-module shape. Agents follow code paths inside a file well. They lose the thread when a flow crosses three modules and a background job. A flowchart in prose closes that gap. The agent sees the whole flow before it reads any of the code, so it knows which file fits where. The artist claim flow on TheBest.Ink touches the artist module, the studio module, the email module, the Instagram OAuth callback, and an LLM moderation step. No single file shows that. The journey does.
Intent disambiguation. "Should this case block the user or just warn them?" is a question I cannot always answer by reading the code. The journey can answer it, because intent is the whole point of the journey doc. When an artist tries to claim a profile and the email domain does not match the studio website, should we hard-block, throttle, or fall back to Instagram OAuth? The code could branch either way and both are technically reasonable. The journey tells the agent which branch matches the product intent.
That last point is the most important one. Code tells the agent what exists. The journey tells the agent what should happen.
There is a real caveat here, and I want to keep it visible. When the journey drifts from the code, the agent believes the wrong thing more confidently than if no doc had existed at all. A wrong map is worse than no map. That is exactly why the freshness story matters.
There is another caveat that matters once this leaves a solo project: ownership.
In my case, the answer is easy. I own the code, the product, the docs, and the tests. In a team, that is not always true. If the journey lives in Git, engineering becomes the default gatekeeper. That can be good because the doc sits close to the implementation, but it can also exclude the people who understand parts of the journey best: product, design, support, QA, or customer success.
So the question is not only "where should this document live?" It is also "who is responsible for keeping it true?"
A repo-based journey doc needs an owner. Maybe that is the engineer changing the flow. Maybe it is a product engineer. Maybe it is product and engineering together in PR review. But it cannot be an orphaned artifact. If nobody owns the journey, putting it next to the code only makes it stale in a more official-looking place.
Documents that describe behavior go stale. That is the oldest problem in technical writing. PRDs go stale because they are pre-build, stakeholder-facing, and nobody reads them after launch. Module READMEs survive because they sit next to the code, and they break in code review when someone changes the public API.
Customer journeys need the same kind of forcing function. My first idea was a 1:1 mapping between each journey and an e2e test. If the artist discovery journey says "unclaimed profile shows the claim dialog, no booking section", there should be a Playwright test with that exact assertion. If the test breaks, either the code is wrong or the journey is wrong. Either way, someone has to look at both.
But the 1:1 idea is probably too simple.
A journey and an e2e test are not naturally the same size. One journey can include branches, emails, background jobs, external services, moderation, retries, and admin decisions. One Playwright test should not necessarily cover all of that. Otherwise the tests become slow, brittle, and hard to debug.
A better version is this: each journey should contain behavioral claims, and the important claims should point to automated coverage where practical.
That coverage can be different depending on the claim:
The journey is not the test. The journey is the spine that connects intent, implementation, and coverage.
This is the part where BDD deserves an honest comparison. A Gherkin .feature file IS the test. Cucumber parses the prose, runs the step definitions, and the build breaks the moment the scenario drifts from the code. The freshness mechanism is automatic and free.
I dropped Gherkin because prose plus a flowchart reads better to both humans and agents than Given / When / Then does, and a flowchart shows branching that linear Gherkin cannot. That trade is deliberate, and I want to be clear about what it cost. I kept the BDD goal of keeping prose and code in sync. I gave up the BDD mechanism that did it for free.
Free prose and Mermaid are more expressive, but they are also more dangerous. They invite narrative drift. Someone can write a beautiful journey that no system actually follows.
So what I am really choosing is readability and system shape over executability. That gives me a better orientation artifact, but a weaker verification mechanism.
What I am left with is a manual mapping between journey claims and automated tests, plus a CI check I have not built yet.
Customer journeys are useful, but they are not enough by themselves.
They do not replace ADRs. A journey can show that a claim goes through moderation, but an ADR can explain why moderation exists and why it was designed that way.
They do not replace domain rules. A journey can show that a suspicious claim is not hard-blocked, but a domain document or test should define what "suspicious" means.
They do not replace analytics, support knowledge, permission models, legal constraints, or operational runbooks.
The journey is the spine. ADRs explain major decisions. Tests verify behavior. Domain docs define rules. The journey links them together.
That is the role I want this document to play.
A few things are not solved, and I think it is more useful to name them than to pretend they are.
File structure at scale. One customer-journeys.md works for five journeys. It will not work for fifty. Do I split by domain (docs/journeys/artists/, docs/journeys/studios/), or keep one file and use anchor links? I do not know what breaks first.
Stable IDs. If a test references journey 3.2 step 4 and I reorder the journeys, the reference rots. I probably need stable IDs per journey and per step, like J-ARTIST-DISCOVERY-04. I have not committed to a scheme yet.
Ownership in teams. In a solo project, I can update the journey when I change the feature. In a team, that responsibility needs to be explicit. Otherwise the file becomes one more abandoned doc with better syntax highlighting.
The limit of automatic checks. A CI check can verify that a journey file and a test file change in the same PR. That is coupling, and a machine can enforce it. It cannot verify that the new Mermaid arrow actually corresponds to the new test assertion. That is semantic alignment, and it needs a human who understands intent. There is no way to close that gap with tooling, at least not reliably. Every doc that is not executable has the same gap. ADRs have it. Module READMEs have it. Conventions files have it. I accept human review as the alignment mechanism for those. Journeys are the same.
So the honest version of the question is not "can I close the semantic gap?" It is "is the gap small enough that code review can hold it?" For five journeys, yes. For fifty, I am not sure.
For now I am running with one file, five journeys, prose plus Mermaid, and a loose intent to map each important journey claim to automated coverage where practical. The cognitive payoff alone, after almost a year of typing the code, has already paid for the writing. Whether the artifact stays useful at fifty journeys, or quietly drifts into another wrong map, is the part I do not yet know.
Some product knowledge is too important to live only in people's heads, tickets, or stale PRDs. For flows that define how the system should behave end-to-end, a repo-based customer journey document can act as shared context for humans, tests, and AI agents.
But it only works if it has ownership, stable IDs, and a review mechanism that keeps it close to reality.