Skip to content

Instantly share code, notes, and snippets.

@toolness
Created February 24, 2026 03:04
Show Gist options
  • Select an option

  • Save toolness/bac39ccefb486729c1480cd7e2b15ab4 to your computer and use it in GitHub Desktop.

Select an option

Save toolness/bac39ccefb486729c1480cd7e2b15ab4 to your computer and use it in GitHub Desktop.
Draft storage design options for issue #7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Draft Storage Design Options</title>
<style>
:root {
--bg: #1a1a2e;
--card: #16213e;
--border: #0f3460;
--text: #e0e0e0;
--muted: #8a8a9a;
--accent: #e94560;
--accent2: #0f3460;
--green: #4ecca3;
--yellow: #f0c040;
--orange: #e07c3a;
--blue: #457b9d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 2rem;
max-width: 1100px;
margin: 0 auto;
}
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; color: #fff; }
h2 { font-size: 1.4rem; margin: 2.5rem 0 1rem; color: #fff; border-bottom: 2px solid var(--border); padding-bottom: 0.5rem; }
h3 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; color: var(--green); }
p, li { color: var(--text); margin-bottom: 0.5rem; }
ul { padding-left: 1.5rem; margin-bottom: 1rem; }
.subtitle { color: var(--muted); font-size: 0.95rem; margin-bottom: 2rem; }
.erd-container {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
overflow-x: auto;
}
.erd-container svg { display: block; margin: 0 auto; }
.flow-step {
display: flex;
align-items: flex-start;
gap: 1rem;
margin: 0.75rem 0;
}
.flow-num {
background: var(--blue);
color: #fff;
border-radius: 50%;
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 700;
flex-shrink: 0;
}
.flow-text { padding-top: 2px; }
.storage-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
vertical-align: middle;
}
.badge-local { background: var(--orange); color: #fff; }
.badge-server { background: var(--green); color: #1a1a2e; }
.badge-none { background: var(--muted); color: #1a1a2e; }
.problem-box {
background: rgba(233, 69, 96, 0.1);
border: 1px solid rgba(233, 69, 96, 0.3);
border-radius: 8px;
padding: 1rem 1.25rem;
margin: 1rem 0;
}
.approach-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem; }
.pro { color: var(--green); }
.con { color: var(--orange); }
.new-field { fill: #4ecca3; font-weight: bold; }
.changed { fill: #f0c040; }
code {
background: rgba(255,255,255,0.08);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85em;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.comparison-table th, .comparison-table td {
padding: 0.6rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.comparison-table th {
color: var(--muted);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.comparison-table td:first-child { font-weight: 600; }
.legend { display: flex; gap: 1.5rem; margin: 1rem 0; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
.legend-swatch { width: 14px; height: 14px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Draft Storage: Current Design & Proposed Changes</h1>
<p class="subtitle">Understanding the data model so we can pick the right approach</p>
<!-- ============================================================ -->
<h2>Current Database Schema</h2>
<p>Four main collections, plus users. Arrows show foreign-key relations.</p>
<div class="erd-container">
<svg viewBox="0 0 900 520" width="900" height="520" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system, system-ui, sans-serif">
<!-- Users table -->
<rect x="30" y="30" width="200" height="190" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="30" y="30" width="200" height="36" rx="8" fill="#457b9d"/>
<rect x="30" y="58" width="200" height="8" fill="#1e2a3a"/>
<text x="130" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">users</text>
<text x="44" y="82" fill="#8a8a9a" font-size="11">id</text><text x="216" y="82" text-anchor="end" fill="#607a8a" font-size="11">pk</text>
<text x="44" y="102" fill="#e0e0e0" font-size="11">email</text>
<text x="44" y="122" fill="#e0e0e0" font-size="11">display_name</text>
<text x="44" y="142" fill="#e0e0e0" font-size="11">target_language</text>
<text x="44" y="162" fill="#e0e0e0" font-size="11">proficiency_level</text>
<text x="44" y="182" fill="#e0e0e0" font-size="11">tts_enabled</text>
<text x="44" y="202" fill="#e0e0e0" font-size="11">can_create_conversations</text>
<!-- Conversations table -->
<rect x="340" y="30" width="220" height="150" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="340" y="30" width="220" height="36" rx="8" fill="#457b9d"/>
<rect x="340" y="58" width="220" height="8" fill="#1e2a3a"/>
<text x="450" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">conversations</text>
<text x="354" y="82" fill="#8a8a9a" font-size="11">id</text><text x="546" y="82" text-anchor="end" fill="#607a8a" font-size="11">pk</text>
<text x="354" y="102" fill="#e0e0e0" font-size="11">participant_a</text><text x="546" y="102" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<text x="354" y="122" fill="#e0e0e0" font-size="11">participant_b</text><text x="546" y="122" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<text x="354" y="142" fill="#e0e0e0" font-size="11">last_message_at</text>
<text x="354" y="162" fill="#e0e0e0" font-size="11">is_composing_user</text><text x="546" y="162" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<!-- Messages table -->
<rect x="340" y="240" width="220" height="140" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="340" y="240" width="220" height="36" rx="8" fill="#457b9d"/>
<rect x="340" y="268" width="220" height="8" fill="#1e2a3a"/>
<text x="450" y="264" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">messages</text>
<text x="354" y="292" fill="#8a8a9a" font-size="11">id</text><text x="546" y="292" text-anchor="end" fill="#607a8a" font-size="11">pk</text>
<text x="354" y="312" fill="#e0e0e0" font-size="11">conversation</text><text x="546" y="312" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; conversations</text>
<text x="354" y="332" fill="#e0e0e0" font-size="11">sender</text><text x="546" y="332" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<text x="354" y="352" fill="#e0e0e0" font-size="11">content</text>
<text x="354" y="372" fill="#e0e0e0" font-size="11">created</text>
<!-- Translations table -->
<rect x="660" y="220" width="220" height="200" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="660" y="220" width="220" height="36" rx="8" fill="#457b9d"/>
<rect x="660" y="248" width="220" height="8" fill="#1e2a3a"/>
<text x="770" y="244" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">translations</text>
<text x="674" y="272" fill="#8a8a9a" font-size="11">id</text><text x="866" y="272" text-anchor="end" fill="#607a8a" font-size="11">pk</text>
<text x="674" y="292" fill="#e0e0e0" font-size="11">message</text><text x="866" y="292" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; messages</text>
<text x="674" y="312" fill="#e0e0e0" font-size="11">target_user</text><text x="866" y="312" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<text x="674" y="332" fill="#e0e0e0" font-size="11">translated_text</text>
<text x="674" y="352" fill="#e0e0e0" font-size="11">breakdown_json</text>
<text x="674" y="372" fill="#e0e0e0" font-size="11">cognates_json</text>
<text x="674" y="392" fill="#e0e0e0" font-size="11">grammar_notes_json</text>
<text x="674" y="412" fill="#e0e0e0" font-size="11">idiom_notes_json</text>
<!-- Drafts table -->
<rect x="30" y="310" width="220" height="190" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="30" y="310" width="220" height="36" rx="8" fill="#457b9d"/>
<rect x="30" y="338" width="220" height="8" fill="#1e2a3a"/>
<text x="140" y="334" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">drafts</text>
<text x="44" y="362" fill="#8a8a9a" font-size="11">id</text><text x="236" y="362" text-anchor="end" fill="#607a8a" font-size="11">pk</text>
<text x="44" y="382" fill="#e0e0e0" font-size="11">conversation</text><text x="236" y="382" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; conversations</text>
<text x="44" y="402" fill="#e0e0e0" font-size="11">sender</text><text x="236" y="402" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; users</text>
<text x="44" y="422" fill="#e0e0e0" font-size="11">content</text>
<text x="44" y="442" fill="#e0e0e0" font-size="11">ai_response</text>
<text x="44" y="462" fill="#e0e0e0" font-size="11">sequence</text>
<text x="44" y="482" fill="#e0e0e0" font-size="11">message</text><text x="236" y="482" text-anchor="end" fill="#607a8a" font-size="11">fk &rarr; messages</text>
<!-- Relationship lines -->
<!-- conversations.participant_a -> users -->
<line x1="340" y1="98" x2="230" y2="82" stroke="#607a8a" stroke-width="1.5" stroke-dasharray="6,3"/>
<!-- conversations.participant_b -> users -->
<line x1="340" y1="118" x2="230" y2="92" stroke="#607a8a" stroke-width="1.5" stroke-dasharray="6,3"/>
<!-- messages.conversation -> conversations -->
<line x1="450" y1="240" x2="450" y2="180" stroke="#607a8a" stroke-width="1.5"/>
<polygon points="450,184 445,194 455,194" fill="#607a8a"/>
<!-- messages.sender -> users -->
<path d="M 340 328 C 280 328, 280 160, 230 160" stroke="#607a8a" stroke-width="1.5" fill="none" stroke-dasharray="6,3"/>
<!-- translations.message -> messages -->
<line x1="660" y1="288" x2="560" y2="310" stroke="#607a8a" stroke-width="1.5"/>
<polygon points="564,308 558,318 568,316" fill="#607a8a"/>
<!-- drafts.conversation -> conversations -->
<path d="M 140 310 C 140 260, 300 200, 340 140" stroke="#607a8a" stroke-width="1.5" fill="none"/>
<polygon points="337,144 346,136 342,146" fill="#607a8a"/>
<!-- drafts.message -> messages -->
<line x1="250" y1="478" x2="340" y2="370" stroke="#607a8a" stroke-width="1.5"/>
<polygon points="337,374 346,366 342,376" fill="#607a8a"/>
</svg>
</div>
<!-- ============================================================ -->
<h2>Current Draft Flow (What Happens Today)</h2>
<p>When a non-fluent user drafts a message, the system uses <strong>two different storage locations</strong> at different stages:</p>
<h3>Stage 1: While Drafting (in-progress)</h3>
<p>The back-and-forth conversation with the AI assistant is stored in the <strong>browser's localStorage</strong>.</p>
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text">User types "annyeonghaseyo" in the compose input <span class="storage-badge badge-local">sessionStorage</span></div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text">User clicks "Draft" &rarr; opens the Drafting Panel</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text">Draft auto-submitted to <code>POST /api/assist</code> &rarr; AI responds with feedback</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text">The conversation <code>[{role:'user', text:'...'}, {role:'ai', text:'...'}]</code> is saved to <span class="storage-badge badge-local">localStorage</span></div></div>
<div class="flow-step"><div class="flow-num">5</div><div class="flow-text">User revises &rarr; repeat steps 3-4 (conversation grows)</div></div>
<h3>Stage 2: On Send (finalized)</h3>
<p>Once the user clicks "Send", two things happen atomically:</p>
<div class="flow-step"><div class="flow-num">6</div><div class="flow-text">A <strong>message</strong> record is created in PocketBase <span class="storage-badge badge-server">server</span><br/>
<em>This triggers translation + push notifications via a backend hook</em></div></div>
<div class="flow-step"><div class="flow-num">7</div><div class="flow-text"><strong>Draft</strong> records are created in PocketBase, linked to the message <span class="storage-badge badge-server">server</span><br/>
<em>These are the historical record of the drafting conversation</em></div></div>
<div class="flow-step"><div class="flow-num">8</div><div class="flow-text">localStorage is cleared <span class="storage-badge badge-none">deleted</span></div></div>
<h3>Stage 3: After Send (viewing history)</h3>
<p>When you view a sent message, you can expand "draft history" to see the back-and-forth that led to it. This reads from the <code>drafts</code> collection (server).</p>
<div class="problem-box">
<strong>The Problem:</strong> During Stage 1, all state lives in localStorage. If the user's session expires, they switch devices, or clear their browser &mdash; the in-progress draft conversation is <strong>lost forever</strong>. The <code>drafts</code> collection only gets populated <em>after</em> sending (Stage 2).
</div>
<!-- ============================================================ -->
<h2>Approach A: Minimal &mdash; Save Draft Entries to Server Incrementally</h2>
<p>The simplest fix: save each drafting-conversation entry to the <code>drafts</code> collection <strong>as it happens</strong>, not just after sending. No changes to the <code>messages</code> collection at all.</p>
<div class="erd-container">
<svg viewBox="0 0 680 300" width="680" height="300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system, system-ui, sans-serif">
<defs>
<filter id="glow-green"><feDropShadow dx="0" dy="0" stdDeviation="2" flood-color="#4ecca3" flood-opacity="0.5"/></filter>
</defs>
<!-- Messages table (unchanged) -->
<rect x="30" y="30" width="220" height="120" rx="8" fill="#1e2a3a" stroke="#457b9d" stroke-width="2"/>
<rect x="30" y="30" width="220" height="36" rx="8" fill="#457b9d"/>
<rect x="30" y="58" width="220" height="8" fill="#1e2a3a"/>
<text x="140" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">messages</text>
<text x="120" y="54" text-anchor="end" fill="rgba(255,255,255,0.4)" font-size="10">unchanged </text>
<text x="44" y="82" fill="#8a8a9a" font-size="12">id</text>
<text x="44" y="102" fill="#e0e0e0" font-size="12">conversation</text>
<text x="44" y="122" fill="#e0e0e0" font-size="12">sender, content, created</text>
<text x="44" y="142" fill="#e0e0e0" font-size="12">...</text>
<!-- Drafts table (changed) -->
<rect x="380" y="30" width="270" height="250" rx="8" fill="#1e2a3a" stroke="#4ecca3" stroke-width="2.5" filter="url(#glow-green)"/>
<rect x="380" y="30" width="270" height="36" rx="8" fill="#3a7a63"/>
<rect x="380" y="58" width="270" height="8" fill="#1e2a3a"/>
<text x="515" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">drafts</text>
<text x="495" y="54" text-anchor="end" fill="rgba(255,255,255,0.6)" font-size="10">modified </text>
<text x="394" y="82" fill="#8a8a9a" font-size="12">id</text>
<text x="394" y="102" fill="#e0e0e0" font-size="12">conversation</text>
<text x="394" y="122" fill="#e0e0e0" font-size="12">sender</text>
<text x="394" y="142" fill="#e0e0e0" font-size="12">content</text><text x="636" y="142" text-anchor="end" fill="#8a8a9a" font-size="10">user's draft text</text>
<text x="394" y="162" fill="#e0e0e0" font-size="12">ai_response</text><text x="636" y="162" text-anchor="end" fill="#8a8a9a" font-size="10">AI's feedback</text>
<text x="394" y="182" fill="#e0e0e0" font-size="12">sequence</text>
<text x="394" y="202" fill="#e0e0e0" font-size="12">message</text><text x="636" y="202" text-anchor="end" fill="#8a8a9a" font-size="10">null while drafting, set on send</text>
<text x="394" y="226" fill="#4ecca3" font-size="12" font-weight="700">+ is_active</text><text x="636" y="226" text-anchor="end" fill="#4ecca3" font-size="10">NEW: true while drafting</text>
<text x="394" y="248" fill="#4ecca3" font-size="12" font-weight="700">+ ai_approved</text><text x="636" y="248" text-anchor="end" fill="#4ecca3" font-size="10">NEW: AI says it's good</text>
<text x="394" y="268" fill="#4ecca3" font-size="12" font-weight="700">+ draft_text</text><text x="636" y="268" text-anchor="end" fill="#4ecca3" font-size="10">NEW: current compose input</text>
<!-- Arrow: drafts.message -> messages (optional) -->
<line x1="380" y1="198" x2="250" y2="120" stroke="#607a8a" stroke-width="1.5" stroke-dasharray="6,3"/>
<text x="300" y="148" fill="#8a8a9a" font-size="10" text-anchor="middle">linked on send</text>
</svg>
</div>
<div class="approach-card">
<h3>How It Works</h3>
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text">User clicks "Draft" &rarr; first draft entry created on server with <code>is_active: true</code>, <code>message: null</code></div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text">AI responds &rarr; draft entry updated with <code>ai_response</code> (and <code>ai_approved</code> if applicable)</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text">User revises &rarr; new draft entry created (same pattern, <code>sequence</code> increments)</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>On Send:</strong> message created normally &rarr; all active draft entries get <code>message</code> set and <code>is_active: false</code></div></div>
<div class="flow-step"><div class="flow-num">5</div><div class="flow-text"><strong>On Cancel:</strong> active draft entries deleted</div></div>
<div class="flow-step"><div class="flow-num">6</div><div class="flow-text"><strong>On Restore:</strong> query <code>drafts</code> where <code>is_active = true</code> for this conversation &rarr; reopen panel with full history</div></div>
<div class="pros-cons">
<div>
<p class="pro"><strong>Pros</strong></p>
<ul>
<li class="pro">No changes to the <code>messages</code> collection at all</li>
<li class="pro">No realtime subscription complexity</li>
<li class="pro">No new access rules needed on messages</li>
<li class="pro">Message creation still happens at send time (same hook fires normally)</li>
<li class="pro">Small migration, small code change</li>
</ul>
</div>
<div>
<p class="con"><strong>Cons</strong></p>
<ul>
<li class="con">The compose-input text needs a home &mdash; stored on the latest active draft entry as <code>draft_text</code></li>
<li class="con">If user types but never clicks "Draft", nothing is saved (same as today)</li>
<li class="con">Draft entries exist without a message during drafting (message is null) &mdash; need to filter these out of draft-history UI</li>
</ul>
</div>
</div>
</div>
<!-- ============================================================ -->
<h2>Approach B: Full &mdash; Draft Messages on the Server</h2>
<p>Add an <code>is_draft</code> boolean to the <code>messages</code> collection. A draft message is created as soon as drafting starts, then "published" by flipping <code>is_draft</code> to <code>false</code> when sent.</p>
<div class="erd-container">
<svg viewBox="0 0 680 360" width="680" height="360" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system, system-ui, sans-serif">
<!-- Messages table (changed) -->
<rect x="30" y="30" width="270" height="160" rx="8" fill="#1e2a3a" stroke="#4ecca3" stroke-width="2.5" filter="url(#glow-green)"/>
<rect x="30" y="30" width="270" height="36" rx="8" fill="#3a7a63"/>
<rect x="30" y="58" width="270" height="8" fill="#1e2a3a"/>
<text x="165" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">messages</text>
<text x="145" y="54" text-anchor="end" fill="rgba(255,255,255,0.6)" font-size="10">modified </text>
<text x="44" y="82" fill="#8a8a9a" font-size="12">id</text>
<text x="44" y="102" fill="#e0e0e0" font-size="12">conversation</text>
<text x="44" y="122" fill="#e0e0e0" font-size="12">sender</text>
<text x="44" y="142" fill="#e0e0e0" font-size="12">content, created</text>
<text x="44" y="166" fill="#4ecca3" font-size="12" font-weight="700">+ is_draft</text><text x="286" y="166" text-anchor="end" fill="#4ecca3" font-size="10">NEW: true until sent</text>
<text x="44" y="184" fill="#f0c040" font-size="11">~ ListRule changed</text><text x="286" y="184" text-anchor="end" fill="#f0c040" font-size="10">hide drafts from other user</text>
<!-- Drafts table (changed) -->
<rect x="380" y="30" width="270" height="230" rx="8" fill="#1e2a3a" stroke="#4ecca3" stroke-width="2.5" filter="url(#glow-green)"/>
<rect x="380" y="30" width="270" height="36" rx="8" fill="#3a7a63"/>
<rect x="380" y="58" width="270" height="8" fill="#1e2a3a"/>
<text x="515" y="54" text-anchor="middle" fill="#fff" font-weight="700" font-size="14">drafts</text>
<text x="495" y="54" text-anchor="end" fill="rgba(255,255,255,0.6)" font-size="10">modified </text>
<text x="394" y="82" fill="#8a8a9a" font-size="12">id</text>
<text x="394" y="102" fill="#e0e0e0" font-size="12">conversation, sender</text>
<text x="394" y="122" fill="#e0e0e0" font-size="12">content</text>
<text x="394" y="142" fill="#e0e0e0" font-size="12">ai_response</text>
<text x="394" y="162" fill="#e0e0e0" font-size="12">sequence</text>
<text x="394" y="182" fill="#e0e0e0" font-size="12">message</text><text x="636" y="182" text-anchor="end" fill="#8a8a9a" font-size="10">links to draft msg immediately</text>
<text x="394" y="206" fill="#4ecca3" font-size="12" font-weight="700">+ is_active</text><text x="636" y="206" text-anchor="end" fill="#4ecca3" font-size="10">NEW: true while drafting</text>
<text x="394" y="228" fill="#4ecca3" font-size="12" font-weight="700">+ ai_approved</text><text x="636" y="228" text-anchor="end" fill="#4ecca3" font-size="10">NEW: AI says it's good</text>
<!-- Arrow: drafts.message -> messages -->
<line x1="380" y1="178" x2="300" y2="130" stroke="#4ecca3" stroke-width="1.5"/>
<text x="326" y="145" fill="#4ecca3" font-size="10" text-anchor="middle">linked immediately</text>
<!-- Backend changes box -->
<rect x="30" y="230" width="620" height="120" rx="8" fill="rgba(240,192,64,0.08)" stroke="#f0c040" stroke-width="1"/>
<text x="50" y="254" fill="#f0c040" font-weight="700" font-size="13">Backend Hook Changes Required</text>
<text x="50" y="278" fill="#e0e0e0" font-size="12">1. OnRecordAfterCreateSuccess("messages"): skip if is_draft = true</text>
<text x="50" y="298" fill="#e0e0e0" font-size="12">2. NEW OnRecordAfterUpdateSuccess("messages"): when is_draft flips false &rarr; trigger translation</text>
<text x="50" y="318" fill="#e0e0e0" font-size="12">3. Add UpdateRule + DeleteRule to messages (sender can update own drafts)</text>
<text x="50" y="338" fill="#e0e0e0" font-size="12">4. ListRule: (is_draft = false || sender = @request.auth.id) &mdash; hide drafts from other user</text>
</svg>
</div>
<div class="approach-card">
<h3>How It Works</h3>
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text">User clicks "Draft" &rarr; a <strong>message</strong> is created with <code>is_draft: true</code>. Backend hook sees the flag and <strong>skips</strong> translation/push.</div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text">Draft entries are created on the server, linked to this draft message immediately</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text">User revises &rarr; message content updated, new draft entry added</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>On Send:</strong> message updated: <code>is_draft: false</code>. Backend hook fires, triggering translation + push. Draft entries marked <code>is_active: false</code>.</div></div>
<div class="flow-step"><div class="flow-num">5</div><div class="flow-text"><strong>On Cancel:</strong> draft message deleted, draft entries deleted</div></div>
<div class="flow-step"><div class="flow-num">6</div><div class="flow-text"><strong>On Restore:</strong> query messages where <code>is_draft = true</code> &rarr; reopen panel with full history</div></div>
<div class="pros-cons">
<div>
<p class="pro"><strong>Pros</strong></p>
<ul>
<li class="pro">Complete server-side state &mdash; the draft message text is always on the server</li>
<li class="pro">Cleaner relationship: draft entries always have a message FK</li>
<li class="pro">The "current text" has a natural home (the message's <code>content</code> field)</li>
<li class="pro">Better for future features (e.g., showing "X is drafting..." to the other user)</li>
</ul>
</div>
<div>
<p class="con"><strong>Cons</strong></p>
<ul>
<li class="con">Must modify <code>messages</code> ListRule/ViewRule to hide drafts from other participant</li>
<li class="con">Must add UpdateRule + DeleteRule to messages (currently none)</li>
<li class="con">Translation hook changes: must gate on <code>is_draft</code>, plus add a new <code>OnRecordAfterUpdateSuccess</code> hook for the draft&rarr;published transition</li>
<li class="con">Realtime subscription edge case: the other user receives a message <em>update</em> event (not <em>create</em>) when the draft is published &mdash; needs careful handling</li>
<li class="con">More moving parts = more things to test</li>
</ul>
</div>
</div>
</div>
<!-- ============================================================ -->
<h2>Side-by-Side Comparison</h2>
<table class="comparison-table">
<thead>
<tr><th>Aspect</th><th>Approach A (Minimal)</th><th>Approach B (Full)</th></tr>
</thead>
<tbody>
<tr>
<td>Collections changed</td>
<td>1 (<code>drafts</code>)</td>
<td>2 (<code>messages</code> + <code>drafts</code>)</td>
</tr>
<tr>
<td>New fields</td>
<td>3 on drafts</td>
<td>1 on messages + 2 on drafts</td>
</tr>
<tr>
<td>Access rule changes</td>
<td>Add UpdateRule/DeleteRule to drafts</td>
<td>Modify ListRule/ViewRule on messages, add UpdateRule/DeleteRule to both</td>
</tr>
<tr>
<td>Backend hook changes</td>
<td>None</td>
<td>Gate create hook on is_draft, add update hook</td>
</tr>
<tr>
<td>Realtime complexity</td>
<td>None &mdash; messages still only created at send</td>
<td>Must handle update events as "new message" for other participant</td>
</tr>
<tr>
<td>Draft survives logout?</td>
<td>Yes (draft entries on server)</td>
<td>Yes (draft message + entries on server)</td>
</tr>
<tr>
<td>Cross-device sync?</td>
<td>Yes (query active drafts on load)</td>
<td>Yes (query draft messages on load)</td>
</tr>
<tr>
<td>Current compose text preserved?</td>
<td>Stored as <code>draft_text</code> on latest active draft entry</td>
<td>Stored as <code>content</code> on the draft message itself</td>
</tr>
<tr>
<td>Estimated complexity</td>
<td>Small</td>
<td>Medium-Large</td>
</tr>
</tbody>
</table>
<div class="legend" style="margin-top: 2rem;">
<div class="legend-item"><div class="legend-swatch" style="background: #4ecca3;"></div> New field / modified table</div>
<div class="legend-item"><div class="legend-swatch" style="background: #f0c040;"></div> Changed rule / hook</div>
<div class="legend-item"><div class="legend-swatch" style="background: #457b9d;"></div> Unchanged</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment