<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ykidus053-create</title>
    <description>The latest articles on DEV Community by ykidus053-create (@ykidus053create).</description>
    <link>https://dev.to/ykidus053create</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4164602%2Ff8fdfeea-66d9-473b-b722-063f618a9f50.png</url>
      <title>DEV Community: ykidus053-create</title>
      <link>https://dev.to/ykidus053create</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ykidus053create"/>
    <language>en</language>
    <item>
      <title>Document automation with no backend: turning Word templates into questionnaires in the browser</title>
      <dc:creator>ykidus053-create</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:47:17 +0000</pubDate>
      <link>https://dev.to/ykidus053create/document-automation-with-no-backend-turning-word-templates-into-questionnaires-in-the-browser-m5i</link>
      <guid>https://dev.to/ykidus053create/document-automation-with-no-backend-turning-word-templates-into-questionnaires-in-the-browser-m5i</guid>
      <description>&lt;p&gt;Law firms, HR teams and freelancers draft the same Word documents over and over: engagement letters, NDAs, offer letters, invoices. Document automation tools exist for this, but the established ones are cloud services. They cost roughly $80 to $400 a month, and they all start with the same step: upload your templates and your clients' details to someone else's server.&lt;/p&gt;

&lt;p&gt;I wanted to see how far the browser alone could go. The result is &lt;a href="https://getclausery.github.io/" rel="noopener noreferrer"&gt;Clausery&lt;/a&gt;: you take a .docx you already use, mark the parts that change with tags, and it turns them into a short questionnaire. Answer it, and the finished Word file is assembled on your own machine. There is no backend at all. The whole product is a folder of static files on GitHub Pages.&lt;/p&gt;

&lt;p&gt;This post covers how it works and the parts that were harder than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  From tags to a questionnaire
&lt;/h2&gt;

&lt;p&gt;A template is an ordinary Word document with tags in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;Dear {client_salutation},

This letter confirms that {firm_name} will represent {client_name} in {matter_description}.
{#has_retainer}We require an advance deposit of {retainer_amount}.{/has_retainer}

{#attorneys}
• {name}, {role}: {rate} per hour
{/attorneys}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docxtemplater.com/" rel="noopener noreferrer"&gt;docxtemplater&lt;/a&gt; does the actual filling, and its inspect module gives me every tag and where it sits. The interesting part is turning that list into questions nobody has to configure by hand. Clausery guesses from the tag names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;client_name&lt;/code&gt; is short text, &lt;code&gt;client_address&lt;/code&gt; is long text, &lt;code&gt;invoice_date&lt;/code&gt; is a date, &lt;code&gt;retainer_amount&lt;/code&gt; is money, &lt;code&gt;term_years&lt;/code&gt; is a number.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{#has_retainer}…{/has_retainer}&lt;/code&gt; is a yes/no question, because of the &lt;code&gt;has_&lt;/code&gt; prefix (also &lt;code&gt;is_&lt;/code&gt;, &lt;code&gt;include_&lt;/code&gt;, &lt;code&gt;needs_&lt;/code&gt; and a few others). Every question inside it is hidden until the box is ticked.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{#attorneys}&lt;/code&gt; with tags inside and a plural name becomes a repeating group: "Add attorney", with name, role and rate for each.&lt;/li&gt;
&lt;li&gt;Tags sharing a prefix (&lt;code&gt;client_*&lt;/code&gt;, &lt;code&gt;party_a_*&lt;/code&gt;) are grouped into sections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The guesses are only a starting point. A designer screen lets you change labels, types, help text and rules, and the library templates ship with a hand-written setup on top of the inferred one.&lt;/p&gt;

&lt;p&gt;Two Word quirks needed handling before any of this worked reliably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tags split across runs.&lt;/strong&gt; Word happily stores &lt;code&gt;{client_name}&lt;/code&gt; as &lt;code&gt;{cli&lt;/code&gt; + &lt;code&gt;ent_na&lt;/code&gt; + &lt;code&gt;me}&lt;/code&gt; in three runs with different formatting. docxtemplater copes with this, which is most of the reason I used it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stray whitespace around section tags.&lt;/strong&gt; A &lt;code&gt;{#has_guarantor}&lt;/code&gt; alone on its own line should disappear along with its paragraph. It does, but only if the tag is the paragraph's &lt;em&gt;only&lt;/em&gt; content, and Word users very often leave a trailing space or tab. That left blank lines all over generated documents. The fix is a small pass over the XML before the engine sees it, trimming whitespace from paragraphs that hold nothing but a section tag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A tag that opens in the first cell of a table row and closes in the last cell repeats the whole row, which is how invoices get a proper line-item table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;| {#line_items}{item_name} | {item_quantity} | {item_rate} | {item_amount}{/line_items} |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A tiny expression language instead of eval
&lt;/h2&gt;

&lt;p&gt;Questions need rules ("show this only if the client is a company") and some fields need calculations (line amount = quantity × rate, then subtotal, tax and total). The obvious shortcut is to let template authors write JavaScript. I didn't want that, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app's Content Security Policy is &lt;code&gt;script-src 'self'&lt;/code&gt; with no &lt;code&gt;unsafe-eval&lt;/code&gt;, so &lt;code&gt;new Function()&lt;/code&gt; is off the table anyway.&lt;/li&gt;
&lt;li&gt;Templates get shared between people. A template should never be able to run code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So there is a small expression language with its own lexer, a recursive-descent parser and an evaluator. Identifiers resolve only against the answers object: there is no access to globals or prototypes. It supports arithmetic, comparisons, &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt;/&lt;code&gt;not&lt;/code&gt;, lists, member access into repeating rows, and 33 functions (&lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;days_between&lt;/code&gt;, &lt;code&gt;words&lt;/code&gt; for amounts in words, and so on). The invoice template's tax line looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;round((subtotal_amount - if(has_discount, discount_amount, 0)) * tax_rate / 100, 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nesting is capped at 100 levels, so a hostile template gets a friendly error instead of a stack overflow. Chains of the same operator (&lt;code&gt;a + b + c + …&lt;/code&gt;) parse to one flat node and don't count towards that limit.&lt;/p&gt;

&lt;p&gt;Evaluation order turned out to be the subtle part. A computed total can depend on a field that is hidden by a rule that depends on another computed field. Clausery builds a dependency graph of every field, including each child of a repeating group, and evaluates in topological order. For each field it first decides visibility, then blanks hidden answers &lt;em&gt;before&lt;/em&gt; anything else reads them. That way a total never includes a discount from a box that was unticked three edits ago. A cycle made only of expressions (&lt;code&gt;a = b + 1&lt;/code&gt;, &lt;code&gt;b = a + 1&lt;/code&gt;) is reported on the fields involved instead of hanging the form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage, encryption and lock-down
&lt;/h2&gt;

&lt;p&gt;Templates, drafts and settings live in IndexedDB. There is an optional "vault" that encrypts the workspace at rest using only WebCrypto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// passphrase → PBKDF2-SHA256, 600,000 iterations → AES-256-GCM key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFKC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PBKDF2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deriveKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PBKDF2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small encrypted verifier tells the app whether a passphrase is right, so neither the passphrase nor the key is ever stored. There is no recovery: lose the passphrase and the data is gone, which is the point.&lt;/p&gt;

&lt;p&gt;"Nothing leaves your browser" is a claim, so the app is built to make it checkable. The Content Security Policy is the real contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default-src 'self'; script-src 'self'; connect-src 'self' https://api.lemonsqueezy.com;
object-src 'none'; base-uri 'self'; form-action 'none'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connect-src&lt;/code&gt; allows the app's own origin, plus a license check that only ever sends a license key if you buy one. There are no analytics or third-party scripts in the app. A service worker precaches the app files, so after the first visit it works offline, and each release ships a new cache version so updates are atomic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live preview
&lt;/h2&gt;

&lt;p&gt;The newest feature renders the actual document beside the questions as you type. Every change (debounced to 600 ms) builds the .docx in memory and hands it to &lt;a href="https://github.com/VolodymyrBaydalka/docxjs" rel="noopener noreferrer"&gt;docx-preview&lt;/a&gt;, which renders it as HTML in a side panel scaled to fit. Unanswered questions show as &lt;code&gt;[Client name]&lt;/code&gt;-style placeholders, so you can see the whole shape of the document from the first second.&lt;/p&gt;

&lt;p&gt;Rendering into a staging element and swapping it in, guarded by a sequence number, stops a slow render from overwriting a newer one when you type quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client intake without a server
&lt;/h2&gt;

&lt;p&gt;Firms often want the client to answer some of the questions. With no server, there is nowhere to host a form. So Clausery exports the questionnaire as a single self-contained HTML file with an inlined runtime. The client opens it offline, fills it in and saves an answers file, which the firm imports into a draft. It is clunkier than a hosted form, but no client data ever touches a third party, including me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;About 3,500 lines of app JavaScript: ES modules, no framework, no build step for the app itself.&lt;/li&gt;
&lt;li&gt;One vendor bundle (docxtemplater, PizZip, docx-preview) of 138 KB gzipped.&lt;/li&gt;
&lt;li&gt;84 unit tests with &lt;code&gt;node:test&lt;/code&gt; (the expression language, evaluation order, the vault, licensing) and 118 Playwright tests covering the app, accessibility checks with axe, and the static site.&lt;/li&gt;
&lt;li&gt;87 free templates in the library, from NDAs to invoices, each with a page showing its full wording.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;p&gt;The weakest part is the step where you add tags in Word. Inference covers common names well, but real firm precedents are messy: tracked changes, content controls, numbering that breaks when a section disappears. If you have a Word file that breaks it, I'd like to see what happens.&lt;/p&gt;

&lt;p&gt;You can try it at &lt;a href="https://getclausery.github.io/" rel="noopener noreferrer"&gt;getclausery.github.io&lt;/a&gt;. The template library is free with no account, and there's a &lt;a href="https://getclausery.github.io/templates/invoice.html" rel="noopener noreferrer"&gt;free invoice template&lt;/a&gt; if you want to see the calculations and live preview without writing any tags.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
