AI with AIlabs
← All guides
3 min read

A Text Editor Is a Security Decision

Autosave, a Mark Done button that never publishes, and an allowlist that decides what gets stored. Building Switchpath's project workspace, step 7.


Build LogClaude CodeTiptapNotionVercel

Learn · Oct 22, 2026

Switchpath tells you the gap between the job you have and the one you want. That part was already live.

But knowing your gap and closing it are two different things. This step gave users an actual place to do the work: briefs, a project workspace, and an editor to write in.

Anything typed into that editor gets saved. Then it shows up on a page someone else can see.

That makes the editor a security decision, not a feature. So I stopped trusting whatever the browser sent me, and rebuilt every document from an allowlist instead.

Every brief needed a place to work

Until this step, Switchpath only analyzed. Upload a resume and a job posting, and it told you the gap. Useful, but it stopped there. Nowhere to actually do the work. So this session added a briefs list, a detail page for each one, and a real workspace inside it. I also linked the analysis cards straight into the matching brief. One click from ‘here’s your gap’ to ‘here’s your work.’

The editor itself was the easy part

I built it with Tiptap, a rich-text editor, with autosave so nobody loses work to a closed tab. Getting a good writing experience on screen is a solved problem. The real question was what happens to everything typed into it, because that content gets stored and shown to other people later.

Rebuild every document from an allowlist

My prompt to Claude Code was specific: don’t trust the editor’s output, and don’t clean it up after the fact. Only explicitly permitted content gets saved. Known block types only. Capped size and depth. Links limited to http, https, and mailto. Nothing the client sends gets stored untouched. Here’s the thing about where you put that check: clean it at render time, and every page that shows the content has to remember to do it. Clean it at the moment of saving, and there’s one door, and it’s guarded.

Finishing isn’t publishing

Mark Done closes a project. It never publishes it. That rule lives in the route name, /done, and in the commit message. Finishing your work and releasing it to the world are two different choices, and the user makes the second one on purpose. Someone switching careers, nervous about being judged? An app that quietly published their half-finished draft would be a betrayal.

A private note, just for adjustments

Each brief also gets a box for flagging that it needs adjusting. These notes stay with the person who wrote them, separate from the project content, and only tune that person’s future briefs. Nobody else sees them. A flagged brief shows a visible notice in the editor too, so the state is never a mystery.

Keep the layers boring

The project module splits into three parts: a document layer that touches data, an HTTP layer that handles requests, and a service layer in between that holds the rules. Same pattern as earlier steps. Consistency is a feature when you’re building with AI. Every new step can copy a shape that already works instead of inventing a new one.

Ask for the test script in the same prompt

This is the part anyone can copy. Ask for a test script in the same prompt as the feature. Throwaway users, no AI calls, and checks that one user can’t touch another user’s project, that bad links get stripped, and that autosave behaves. It turns ‘it seems to work’ into PASS or FAIL.

What broke: Nothing crashed this time, and I don’t want to dress that up as a dramatic save. The real risk was invisible: a working editor that would have happily stored whatever the browser handed it. If I’d judged this step by whether typing and saving worked, it would’ve looked done while quietly leaving the door open. What nearly went wrong was my definition of done. Not the code.

The fix: I changed the definition of done. For any feature that stores user content, the prompt now asks for three things together: an allowlist applied at save, explicit limits on size and depth, and a throwaway-user test script that proves one person can’t reach another person’s data. The editor only counts as finished when that script prints PASS.

The prompt I now use for anything that stores user content
I am adding a rich-text editor where users write content that is stored and later shown to other people.

Build the feature, and in the same pass:
1. Add a sanitizer that runs on save. Rebuild every document from an allowlist: list the block types you allow, cap total size and nesting depth, and permit links only with http, https or mailto. Never store anything the client sends untouched.
2. Make the sanitizer the single place content is cleaned, so display code can trust what it reads.
3. Write a test script that creates throwaway users and makes no AI calls. It must check that user A cannot read or edit user B's project, that disallowed links are stripped, that oversized or too-deep content is rejected or trimmed, and that autosave saves and does not duplicate.
4. Print PASS or FAIL for each check and exit non-zero if any fail.

Before writing code, tell me which block types you plan to allow so I can approve the list.

Get new guides and builds in your inbox

No noise. New artifacts, skills, and lessons when they drop — and early access when agents launch.

next_steps.exe

Want a full structured path instead of one-off guides?

That's Learn AI with AI — a sequenced course that takes you from zero to building real things, with community and accountability built in.

Check out Learn AI with AI →