Draftmount

Devlog 005: Under the Hood

A nerdier month: document state, autosave, access, exports, and a peek at the code.

Hello again, writers!

October was a technical month. After months on the editor, the interface and manuscript organization, I turned my attention to what happens underneath all of it.

Making a writing app look nice is the comparatively easy part. Making sure it behaves when someone has written 90,000 words, rearranged twelve chapters and accidentally closed their browser is a different adventure. This month was about architecture, performance, data integrity, and getting ready for the beta.

Fair warning: we’re getting nerdy today. This post includes a few small excerpts from the editor’s code and a couple of before-and-after comparisons.

beforeAn early layout: a grey toolbar with a Save button, a dropdown for chapters, and a plain boxed text area in Times New Roman.Before: an early layout
afterThe Draftmount studio: a binder of chapters and scenes on the left, a paper page in the middle, and Ideas, Cast and Goals tabs on the right, all on grid paper.After: the Draftmount studio
The studio, then and now.

The editor and its many moving parts

Rich-text editors are surprisingly complicated. A manuscript holds far more than the words on screen: paragraphs, headings, formatting, selections, cursor positions, and the structure that ties them together. All of it has to behave consistently.

I reviewed how changes move through the editor and into the rest of the workspace. Switching chapters should keep the right content and formatting. Reorganizing scenes shouldn’t touch what’s inside them. And typing shouldn’t get slower just because the manuscript got longer.

One piece that made a real difference is how saves are scheduled. Every scene, chapter title and note gets its own little timer, so a pending save for one scene can’t be cancelled or overwritten by typing in another:

Studio editorexcerpt · tsx
/** Debounce per key (one timer per scene/item). */
function useDebouncedCalls(ms: number) {
  const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
  useEffect(() => () => { timers.current.forEach(clearTimeout); }, []);
  return useCallback((key: string, fn: () => void) => {
    const t = timers.current.get(key);
    if (t) clearTimeout(t);
    timers.current.set(key, setTimeout(() => {
      timers.current.delete(key);
      fn();
    }, ms));
  }, [ms]);
}

// in the editor:
debounce(`text:${sceneId}`, () => saveText(sceneId, text, toHtml(text)));

Keyed debouncing: switching scenes mid-save no longer loses the last few words of the scene you left.

Rethinking how manuscripts are stored

A manuscript needs a stable identity. Chapters and scenes have to keep their relationships when they move, and metadata like titles, word counts and ordering must stay consistent with the actual content.

The answer was to keep a scene’s place in the book separate from its words. Moving a scene changes two numbers. It never rewrites the text.

Each scene keeps a stable identity that never changes or gets reused. Its position in the book is stored as a number that can sit between two others, so reordering never means renumbering everything. The word count is saved alongside it, and deleting something marks it as deleted first rather than erasing it on the spot, so nothing vanishes by accident.

Then there’s recovery. Browsers crash, tabs close, devices run out of space, and people clear their browser data. Local storage has limits, so I treat backup and export as core features, not optional extras.

The problem with autosave

Autosave sounds wonderful until you think about everything that can go wrong. What happens when someone types non-stop for twenty minutes? What if they switch chapters while a save is still pending? What if two updates arrive in the wrong order?

The rule I settled on: a reassuring little “Saved” needs to mean something. If the latest changes haven’t actually been stored, the app shouldn’t pretend they have. So every save lands on the device first, joins a queue, and only leaves the queue once it has really arrived:

Savingexcerpt · ts
/** Save locally at once, then to the cloud.
 *  Failed saves stay queued and retry on flush(). */
export async function saveText(sceneId: string, text: string, html: string) {
  cacheText(sceneId, text, html);                 // 1. on this device, immediately
  const q = readQueue().filter((p) => p.sceneId !== sceneId);
  q.push({ sceneId, text, html, at: Date.now() }); // 2. newest version wins the queue
  writeQueue(q);
  return (await flush()) ? 'cloud' : 'queued';     // 3. only "cloud" once it's there
}
beforeEarly status bar showing a bright green “Saved!” badge next to a text box, with no time or location.Before: an early save indicator
afterThe Draftmount writing page with a handwritten status at the bottom reading “saved in this browser” and the time.After: says where, and when
The save indicator. The early version flashed “Saved!” on every keystroke whether or not anything had been stored. Now the status changes only after a save has actually finished.

Authentication and access

Welcoming testers brings a new set of responsibilities: session handling, access permissions, and keeping account information separate from manuscript content. Rather than reinvent everything, Draftmount uses established services for identity and infrastructure.

Signing in proves who you are; it doesn’t decide what you can use. Beta codes are checked on the server, and every request for a feature checks your plan there too, regardless of what the interface happens to show. Hiding a button is a courtesy. The real decision happens somewhere a browser can’t change it.

Exporting is more complicated than it looks

Turning Draftmount’s internal structure into a portable file means headings, paragraph breaks, emphasis and chapter boundaries all have to be handled on purpose. Keeping the export code separate from the editor makes it easier to add formats without tying them to how text happens to look in the browser, and gives me one clear place to test formatting.

Exportexcerpt · ts
export function toMarkdown(p: Project): string {
  const lines = [`# ${p.title}`, ''];
  for (const c of p.chapters) {
    lines.push(`## ${c.title}`, '');
    for (const s of c.scenes) lines.push(`### ${s.title}`, '', s.body.trim(), '');
  }
  return lines.join('\n');
}

A manuscript belongs to its writer, and being able to take it elsewhere is part of that.

Performance testing and the little horrors

Small test documents are lovely, and they hide performance problems beautifully. I’ve been watching editor responsiveness, navigation, memory use, and the cost of recalculating things like word counts.

One easy win: only the scene you’re typing in gets counted live. Every other scene uses the count saved with it, so a long book doesn’t make each keystroke slower:

Word countsexcerpt · ts
const liveWords = (s: Scene) => (s.id === sceneId ? words(body) : s.words);
const bookWords = detail.scenes.reduce((n, s) => n + liveWords(s), 0);

Performance work has its own hazard: spending three hours making something faster, only to find the real problem was somewhere else entirely. Deeply educational. I recommend it to nobody.

Keeping the first release manageable

I also had to decide what Draftmount actually needs for its first beta, separating what’s essential to writing from what can arrive later, and mapping which parts of the app depend on which.

Cloud sync is the clearest example. It brings far more complexity than local documents: conflict resolution, network failures, recovery, and changes arriving from several devices at once. Those problems are worth solving carefully, so for the first testing period I’d rather ship a smaller set of dependable tools with clearly stated limits.

Looking ahead to November

Draftmount is close to becoming something writers can try. November is about the beta experience: onboarding, documenting known limits, and making sure the most important workflows are ready for real feedback. I’ll also spend time on the website, accounts and the infrastructure around the app.

There’s a lot left to do, but watching the technical pieces come together has been incredibly satisfying, even if I’ve developed a deeply personal rivalry with browser storage.

Until the next draft,

The Draftmount Developer

Development status
Architecture refinement and beta infrastructure planning
Technical focus
Rich-text document state, local persistence, autosave, authentication, export architecture and performance
Next milestone
Beta infrastructure, onboarding and controlled testing