<?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: Anton Tyshchenko</title>
    <description>The latest articles on DEV Community by Anton Tyshchenko (@anton-tyshchenko).</description>
    <link>https://dev.to/anton-tyshchenko</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2540717%2Faab00b2c-40c4-4293-9fff-6e525a0ced07.JPG</url>
      <title>DEV Community: Anton Tyshchenko</title>
      <link>https://dev.to/anton-tyshchenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anton-tyshchenko"/>
    <language>en</language>
    <item>
      <title>How I Made a Free Automated PDF Invoice Maker for Batch Billing All My Contractors</title>
      <dc:creator>Anton Tyshchenko</dc:creator>
      <pubDate>Tue, 14 Apr 2026 15:39:58 +0000</pubDate>
      <link>https://dev.to/gdoc/how-i-made-a-free-automated-pdf-invoice-maker-for-batch-billing-all-my-contractors-1jha</link>
      <guid>https://dev.to/gdoc/how-i-made-a-free-automated-pdf-invoice-maker-for-batch-billing-all-my-contractors-1jha</guid>
      <description>&lt;h2&gt;
  
  
  Why I needed this in the first place
&lt;/h2&gt;

&lt;p&gt;I run a platform for distributing game graphics called Craftpix. Everyone we work with is a contractor. Incredibly creative people who, to put it mildly, have no interest in paperwork. I outsourced bookkeeping and auditing to a team in Singapore. They have their own dashboard and their accountants are meticulous about collecting everything the audit requires. But the invoices for every single transaction are on me.&lt;/p&gt;

&lt;p&gt;In theory that means collecting invoices from each contractor. In practice it would be a nightmare. What saved me is that Hong Kong allows self-billing: I can issue invoices on behalf of my contractors for transactions that have already been completed, without any involvement on their end.&lt;/p&gt;

&lt;p&gt;But even with all the details and Google Docs templates on hand, every invoice still needs updated dates and figures. A pointless waste of time. It makes no difference whether I do it or someone else does. It simply should not exist.&lt;/p&gt;

&lt;p&gt;I am genuinely glad that e-invoicing is picking up in more countries. That is the future. But while we still live in a world where PDF invoices are a legal requirement, you work with what you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client and contractor data: who is responsible for what
&lt;/h2&gt;

&lt;p&gt;When you receive someone's personal data, you become their data controller. That means you are responsible for tracking where that data goes next.&lt;/p&gt;

&lt;p&gt;If you send invoice details to a third-party server-side generator, you are now obligated to read through the Data Processing Agreement of that service. And nobody guarantees things will not go wrong. SaaS services leak data regularly: &lt;a href="https://techcrunch.com/2025/10/03/hacking-group-claims-theft-of-1-billion-records-from-salesforce-customer-databases/" rel="noopener noreferrer"&gt;here is a recent example&lt;/a&gt;, and &lt;a href="https://www.yahoo.com/news/articles/nearly-180k-records-exposed-billing-161600225.html" rel="noopener noreferrer"&gt;here is another one&lt;/a&gt;. Your clients' and contractors' personal data could easily end up on that list.&lt;/p&gt;

&lt;p&gt;When we built our invoice generator, we made a clear decision from the start: user data is none of our business. Nothing goes to the server. Generation happens entirely in the browser using plain JS, and all data stays there. You can open the Network tab and verify for yourself that no personal data leaves your machine.&lt;/p&gt;

&lt;p&gt;The generator is still a form though: one document at a time. So I wrote a small addition that takes an array of invoice data and generates all of them in one go. I keep the array saved, run it once a month, update the numbers, done. All the details stay in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: running the generator
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://gdoc.io/free-online-invoice-generator/" rel="noopener noreferrer"&gt;Free PDF Invoice Maker&lt;/a&gt; and the browser console. I use Chrome: on Mac it is &lt;code&gt;Cmd + Option + J&lt;/code&gt;, on Windows &lt;code&gt;Ctrl + Shift + J&lt;/code&gt;. Go to the Console tab, paste the code below and hit Enter.&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="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// grab the invoice API exposed by gdoc.io on the window object&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoiceAPI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// expose batchInvoice globally so you can call it from the console&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batchInvoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for &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;inv&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="c1"&gt;// reset the form before filling in the next invoice&lt;/span&gt;
            &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// fill in all invoice fields via the store&lt;/span&gt;
            &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inv.number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dueDate.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paymentTerms.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentTerms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;poNumber.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;poNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billTo.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;terms.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

                &lt;span class="c1"&gt;// tax, discount and shipping are optional:&lt;/span&gt;
                &lt;span class="c1"&gt;// if you pass a value, the field gets enabled automatically&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tax.enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;0&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;tax.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount.enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;0&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;discount.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount.mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discountMode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'abs' for fixed amount, 'pct' for percent&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipping.enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shipping&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;0&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;shipping.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shipping&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amountPaid.value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountPaid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="c1"&gt;// each line item needs a unique key — built from timestamp + random suffix&lt;/span&gt;
            &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`items.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.name`&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`items.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.qty`&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`items.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.price`&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="c1"&gt;// save state, then trigger PDF download&lt;/span&gt;
            &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✅ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; invoices generated`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌋 gdoc.io Invoice Volcano activated! Feed batchInvoice([...]) and watch it erupt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator is ready. Now pass it your data array and hit Enter again.&lt;/p&gt;

&lt;p&gt;Each invoice object supports the following fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Invoice number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;currency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Currency symbol, e.g. &lt;code&gt;$&lt;/code&gt;, &lt;code&gt;€&lt;/code&gt;, &lt;code&gt;£&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Invoice date in &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dueDate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Payment due date in &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paymentTerms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;e.g. &lt;code&gt;Net 30&lt;/code&gt;, &lt;code&gt;Net 60&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;poNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Purchase order number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Sender details. Use &lt;code&gt;\n&lt;/code&gt; for line breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;billTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Recipient details. Use &lt;code&gt;\n&lt;/code&gt; for line breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Array of line items, each with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;qty&lt;/code&gt;, and &lt;code&gt;price&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Discount value. Requires &lt;code&gt;discountMode&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discountMode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pct&lt;/code&gt; for percentage, &lt;code&gt;abs&lt;/code&gt; for fixed amount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tax&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Tax rate in percent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shipping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Shipping cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;amountPaid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Amount already paid, shown as a deduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Payment instructions or any additional notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Terms and conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;batchInvoice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// invoice number&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// currency symbol shown on the invoice&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-04-11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// invoice date (YYYY-MM-DD)&lt;/span&gt;
        &lt;span class="na"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-05-11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// payment due date&lt;/span&gt;
        &lt;span class="na"&gt;paymentTerms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Net 30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// payment terms label&lt;/span&gt;
        &lt;span class="na"&gt;poNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PO-2026-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// purchase order number (optional)&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme Corp&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;100 Main St&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;New York, NY 10001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// sender details, use \n for line breaks&lt;/span&gt;
        &lt;span class="na"&gt;billTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TechStart Inc&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;456 Oak Ave&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Boston, MA 02101&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// recipient details&lt;/span&gt;
        &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="c1"&gt;// each item: service/product name, quantity, unit price&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;Frontend Development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&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;Backend API&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// discount value&lt;/span&gt;
        &lt;span class="na"&gt;discountMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 'pct' = percentage, 'abs' = fixed amount&lt;/span&gt;
        &lt;span class="na"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// tax rate in percent&lt;/span&gt;
        &lt;span class="na"&gt;amountPaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// amount already paid (shown as a deduction)&lt;/span&gt;
        &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wire: Bank of America&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Acc: 1234567890&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// payment instructions or any notes&lt;/span&gt;
        &lt;span class="na"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Late payments subject to 1.5% monthly interest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// terms and conditions&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;002&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;€&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-04-11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-06-11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;paymentTerms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Net 60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Digital Studio GmbH&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Berlinstr. 42&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Berlin, 10115&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;billTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MegaShop EU&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;8 Rue de Rivoli&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Paris, 75001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&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;UI/UX Design&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;95&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;Brand Guidelines&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// fixed discount of €400&lt;/span&gt;
        &lt;span class="na"&gt;discountMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// shipping cost (optional)&lt;/span&gt;
        &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IBAN: DE89 3704 0044 0532 0130 00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment within 60 days&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generation starts automatically. Depending on your OS and browser settings, files will either land straight in your downloads folder or the browser will ask for permission on each one.&lt;/p&gt;

&lt;p&gt;Used to be an hour of repetitive work every month. Now I open the array file, update the dates and numbers, run it — the whole thing takes a couple of minutes.&lt;/p&gt;

&lt;p&gt;The full script is also on &lt;a href="https://github.com/anton-tyshchenko/batch-pdf-invoice-generator" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to grab it directly.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Anton Tyshchenko</dc:creator>
      <pubDate>Thu, 10 Jul 2025 17:54:42 +0000</pubDate>
      <link>https://dev.to/anton-tyshchenko/-3792</link>
      <guid>https://dev.to/anton-tyshchenko/-3792</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl" class="crayons-story__hidden-navigation-link"&gt;We Acquired the Rights to a Hardcore Platformer and Released It as a Free Browser Game [Personal Experience]&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/yoka-games"&gt;
            &lt;img alt="Yoka Games logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10972%2F00f64772-25b3-4772-a44f-11e0805103e5.png" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/anton-tyshchenko" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2540717%2Faab00b2c-40c4-4293-9fff-6e525a0ced07.JPG" alt="anton-tyshchenko profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/anton-tyshchenko" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Anton Tyshchenko
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Anton Tyshchenko
                
              
              &lt;div id="story-author-preview-content-2513105" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/anton-tyshchenko" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2540717%2Faab00b2c-40c4-4293-9fff-6e525a0ced07.JPG" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Anton Tyshchenko&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/yoka-games" class="crayons-story__secondary fw-medium"&gt;Yoka Games&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 22 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl" id="article-link-2513105"&gt;
          We Acquired the Rights to a Hardcore Platformer and Released It as a Free Browser Game [Personal Experience]
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/unity3d"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;unity3d&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/html5games"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;html5games&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gameporting"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gameporting&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              4&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>html5games</category>
      <category>gameporting</category>
    </item>
    <item>
      <title>We Acquired the Rights to a Hardcore Platformer and Released It as a Free Browser Game [Personal Experience]</title>
      <dc:creator>Anton Tyshchenko</dc:creator>
      <pubDate>Thu, 22 May 2025 04:16:25 +0000</pubDate>
      <link>https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl</link>
      <guid>https://dev.to/yoka-games/we-acquired-the-rights-to-a-hardcore-platformer-and-released-it-as-a-free-browser-game-personal-2lnl</guid>
      <description>&lt;p&gt;Pukan, Bye-Bye! is a minimalist hardcore platformer where death at every step isn’t a bug but a core mechanic. The game has long been available on various platforms, including Steam and consoles, and now it’s freely accessible in the browser at &lt;a href="https://yoka.net" rel="noopener noreferrer"&gt;yoka.net (Yoka Games)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wanted to Enter Game Development — Without Lengthy Development
&lt;/h2&gt;

&lt;p&gt;It all started with a desire to dive into the gaming industry but without spending months creating a game from scratch. I wasn’t planning to immediately develop a large indie project or launch a team. I wanted to go through the entire cycle “from release to publication” using an existing game.&lt;/p&gt;

&lt;p&gt;I was acquainted with Artalasky — the author of Pukan, Bye-Bye! — through collaboration on the &lt;a href="https://craftpix.net" rel="noopener noreferrer"&gt;Craftpix.net&lt;/a&gt; project. I knew he already had several completed games, and one of them fit my needs perfectly. We quickly found common ground, discussed the details, and I acquired a sublicense for the web version of the game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Construct Over Unity
&lt;/h2&gt;

&lt;p&gt;The original game was developed in Unity. However, Unity WebGL turned out to be entirely unsuitable for my needs: fast loading, instant start, cross-device compatibility. The final build was over 40 megabytes — players simply wouldn’t wait for it to load.&lt;/p&gt;

&lt;p&gt;I needed an engine that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;produces micro-builds (a few megabytes),&lt;/li&gt;
&lt;li&gt;launches instantly,&lt;/li&gt;
&lt;li&gt;is perfectly suited for the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose Construct, with which I already had experience. Rebuilding the game from scratch turned out to be the only viable solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Rebuilt Everything Manually
&lt;/h2&gt;

&lt;p&gt;We had all the Unity source files — from graphics to levels. But the code, physics, character behavior, interface, effects — everything had to be redone.&lt;/p&gt;

&lt;p&gt;The developer Alven took on the task of porting, and he released a detailed video on his channel about how it all happened. He meticulously recreated the mechanics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controls considering inertia, sliding, and air behavior;&lt;/li&gt;
&lt;li&gt;all traps, which were implemented in Unity through animations;&lt;/li&gt;
&lt;li&gt;visual effects, as much as Construct’s capabilities allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result — 166 hours of pure work. The finished game weighs only 1.5 MB and works excellently in the browser on both PC and mobile devices. We even implemented mobile controls from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Aimed to Achieve
&lt;/h2&gt;

&lt;p&gt;This project became my entry into game development from a producer’s perspective. I wanted to gain quick and practical experience in game distribution: understand how releases work, what publication channels exist, where to gather feedback, and how publishers operate at a basic level.&lt;/p&gt;

&lt;p&gt;Additionally, I wanted to assess what kind of team is actually needed to create and release a small but complete project like Pukan, Bye-Bye!.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Came Out of It
&lt;/h2&gt;

&lt;p&gt;Pukan, Bye-Bye! is now available for free — no downloads, no lags:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yoka.net/pukan-bye-bye-free-to-play-online-game/" rel="noopener noreferrer"&gt;Pukan Bye-bye! free-to-play&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ExM1Bh2EViM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>html5games</category>
      <category>gameporting</category>
    </item>
    <item>
      <title>How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)</title>
      <dc:creator>Anton Tyshchenko</dc:creator>
      <pubDate>Sun, 29 Dec 2024 06:05:19 +0000</pubDate>
      <link>https://dev.to/anton-tyshchenko/how-to-create-an-auto-failover-load-balancer-for-100-uptime-3gjp</link>
      <guid>https://dev.to/anton-tyshchenko/how-to-create-an-auto-failover-load-balancer-for-100-uptime-3gjp</guid>
      <description>&lt;p&gt;Recently, my primary server unexpectedly went offline due to unplanned maintenance at the data center. The downtime led to a significant loss of traffic and ad revenue, and I couldn’t intervene in time. This experience made me realize the importance of a robust failover system, especially for situations when immediate action isn’t possible—like during the night or while traveling.&lt;/p&gt;

&lt;p&gt;I sought a straightforward yet dependable solution to ensure nearly 100% uptime, even if one server failed. The goal was to minimize configuration complexity while maximizing reliability. To achieve this, I configured automated site replication across two virtual servers and deployed a failover Load Balancer to seamlessly redirect traffic to the backup server in case of primary server downtime.&lt;/p&gt;

&lt;p&gt;This tutorial is designed for anyone looking to quickly set up a failover system without needing extensive knowledge of network configurations. The steps are straightforward and easy to follow, ideal for users familiar with basic SSH commands. Whether you’re a developer, a website owner, or someone new to server management, this guide will help you save time and ensure your project’s reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is load balancer for?
&lt;/h2&gt;

&lt;p&gt;It receives incoming requests and distributes them among multiple servers. The Load Balancer continuously monitors each server’s status and automatically removes any that go offline, ensuring users are always directed to a functional server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools and Services Used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two virtual machines on DigitalOcean:&lt;/strong&gt; Provides a scalable and reliable cloud environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare for Load Balancer:&lt;/strong&gt; Ensures efficient traffic distribution and failover support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debian 11 for the virtual machines:&lt;/strong&gt; A stable and widely used Linux distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISPManager (trial):&lt;/strong&gt; A user-friendly control panel to simplify server management.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create Droplets
&lt;/h2&gt;

&lt;p&gt;When setting up a Droplet, consider the following key factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Choose a region that is geographically close to your target audience to reduce latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operating System:&lt;/strong&gt; Debian 11 x64, as this tutorial is specifically tailored for this version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM:&lt;/strong&gt; At least 2 GB, which is the minimum requirement for the smooth operation of ispmanager.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screenshot shows an example of the minimal configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1wfxsptznqbeuh1wfah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1wfxsptznqbeuh1wfah.png" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose Authentication Method : Password&lt;/p&gt;

&lt;p&gt;I created two virtual machines right away to demonstrate the process. If you already have a live website, you only need one backup virtual machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing ispmanager
&lt;/h2&gt;

&lt;p&gt;Before installing ispmanager, you need to adjust the hostname to meet the control panel’s requirements. To do this, edit the following file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here you can see the hostname we set when creating the virtual machine: in my case, it’s debian-s1 and debian-s2. Simply add .ltd at the end to form a valid domain name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this file, you need to find the line that specifies the virtual machine name you set when creating it. For example, we'll use debian-s1 here, but you can replace it with your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.1.1 debian-s1 debian-s1
127.0.0.1 localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, add the .ltd suffix to the first entry, and you'll end up with something 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;127.0.1.1 debian-s1.ltd debian-s1
127.0.0.1 localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, we restart the hostname settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart systemd-hostnamed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system is ready for the control panel installation.&lt;/p&gt;

&lt;p&gt;We install wget to download the installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt install wget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download the ISPmanager installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://download.ispmanager.com/install.sh -O install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sh install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the following message appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which version would you like to install ?
b) beta version - has the latest functionality
s) stable version - time-proved version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вам нужно вписать &lt;code&gt;s&lt;/code&gt; и нажать enter&lt;/p&gt;

&lt;p&gt;Type &lt;code&gt;s&lt;/code&gt; and press Enter.&lt;/p&gt;

&lt;p&gt;Next, the system will ask you to choose a preferred version. Choose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) Ispmanager-lite,pro,host with recommended software
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the choice "Which web server would you like to install?" select:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) Nginx + Apache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For "Choose database server for ispmanager's internal data," select:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2) MySQL (recommended when maintaining a large number of sites and users)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The panel installation and component configuration will now begin. This process may take about 10 minutes, so feel free to grab a coffee.&lt;/p&gt;

&lt;p&gt;After the installation is complete, the console will display a message with your IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=================================================
ispmanager-lite-common is installed
Go to the "https://67.205.135.37:1500/ispmgr" to login
Login: root
Password: &amp;lt;root password&amp;gt;
=================================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting a Website
&lt;/h2&gt;

&lt;p&gt;Next, log in to the control panel at https://&lt;strong&gt;your_server_IP&lt;/strong&gt;:1500/ispmgr. You can activate the trial version of the control panel at &lt;a href="https://www.ispmanager.com/" rel="noopener noreferrer"&gt;https://www.ispmanager.com/&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;After logging in, create a user with the same name as the one on your primary server from which the files will be copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Backup Server to Connect to the Primary Server
&lt;/h2&gt;

&lt;p&gt;Before setting up synchronization, you need to create an SSH access key for your primary server. On the backup server, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -C "rsync_backup"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;You can leave the password blank.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, send this key to the primary server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-copy-id -i ~/.ssh/id_rsa.pub -p 22 root@IP-address_of_main_server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! Your backup server can now connect to the primary server without a password. It's time to create bash scripts for automatic copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Automatic Copying
&lt;/h2&gt;

&lt;p&gt;To automatically copy the website files and database, you need to create two scripts and configure a cron job. Here’s how:&lt;/p&gt;

&lt;h3&gt;
  
  
  File Synchronization
&lt;/h3&gt;

&lt;p&gt;Create a folder for the scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p /root/scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a script for copying files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /root/scripts/copy_files.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rsync -avz -e "ssh -p 22" root@IP-address_of_main_server:/var/www/USER_FOLDER/data/www/DOMAIN.LTD/ /var/www/USER_FOLDER/data/www/DOMAIN.LTD/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the placeholders with your data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP-address_of_main_server&lt;/li&gt;
&lt;li&gt;USER_FOLDER&lt;/li&gt;
&lt;li&gt;DOMAIN.LTD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to exclude certain data from synchronization, add these operators after &lt;code&gt;rsync&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--exclude 'wp-config.php'&lt;/code&gt; to exclude a specific file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--exclude 'wp-content/cache/'&lt;/code&gt; to exclude a folder’s contents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database Synchronization
&lt;/h3&gt;

&lt;p&gt;Create a script for copying the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /root/scripts/backup_db.sh 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

# Settings
PRIMARY_HOST="192.168.1.1"
PRIMARY_HOST_PORT="22"
PRIMARY_HOST_USER="root"

PRIMARY_DB_USER="db-user"
PRIMARY_DB_PASSWORD="******"
PRIMARY_DB="data-base-name"

SECONDARY_HOST="localhost"
SECONDARY_DB_USER="db-user"
SECONDARY_DB_PASSWORD="******"
SECONDARY_DB="data-base-name"

echo "$(date) - Starting database synchronization"

# hecking the availability of the Primary server
if ! ssh -q -p $PRIMARY_HOST_PORT $PRIMARY_HOST_USER@$PRIMARY_HOST exit; then
    echo "$(date) - Primary server ($PRIMARY_HOST) is unreachable. Sync aborted."
    exit 1
fi

# Exporting data from the Primary server
echo "$(date) - Exporting data from primary server..."
ssh -p $PRIMARY_HOST_PORT $PRIMARY_HOST_USER@$PRIMARY_HOST "mysqldump -u $PRIMARY_DB_USER -p'$PRIMARY_DB_PASSWORD' $PRIMARY_DB" &amp;gt; /tmp/$PRIMARY_DB.sql

if [ $? -ne 0 ]; then
    echo "$(date) - Failed to export data from primary server. Sync aborted."
    exit 1
fi

# Importing data to the SECONDARY_HOST
echo "$(date) - Importing data to secondary server..."
mysql -u $SECONDARY_DB_USER -p"$SECONDARY_DB_PASSWORD" $SECONDARY_DB &amp;lt; /tmp/$PRIMARY_DB.sql

if [ $? -eq 0 ]; then
    echo "$(date) - Database synchronization completed successfully."

        # Check if the directory exists
        if [ -d "$TARGET_DIR" ]; then
            # Remove all contents of the directory
            rm -rf "${TARGET_DIR:?}"/*
            echo "Contents of $TARGET_DIR have been removed."
        else
            echo "Directory $TARGET_DIR does not exist."
        fi
else
    echo "$(date) - Database synchronization failed."
    exit 1
fi

# Deleting the temporary file
rm -f /tmp/$PRIMARY_DB.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: This script is not suitable for large databases or systems with frequent updates. It works best for resources with occasional updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up a CRON Job for Synchronization
&lt;/h3&gt;

&lt;p&gt;Set the frequency of the synchronization scripts based on how fresh you want the backup server data to be. For my site, which updates only during the day, I chose daily synchronization during off-hours.&lt;/p&gt;

&lt;p&gt;To create a cron job, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /var/spool/cron/crontabs/root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following lines to the end of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Daily MySQL copy (5:00 AM Bangkok time)
0 22 * * * bash /root/scripts/backup_db.sh
## Daily site copy (5:05 AM Bangkok time)
5 22 * * * bash /root/scripts/copy_files.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an initial copy of the site and its database, run these commands once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash /root/scripts/backup_db.sh
bash /root/scripts/copy_files.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! If you’ve reached this point, you now have a fully operational backup server. The last step is to activate the Load Balancer for failover in case the primary server goes down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Load Balancer
&lt;/h2&gt;

&lt;p&gt;This section explains how to set up a Load Balancer using Cloudflare. I assume your site is already connected to Cloudflare and DNS records are configured. If not, complete the basic setup first.&lt;/p&gt;

&lt;p&gt;To make the system activate only when the primary server fails, follow these steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create one Load Balancer;&lt;/li&gt;
&lt;li&gt;Configure two server pools;&lt;/li&gt;
&lt;li&gt;Add one endpoint per pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go to the Traffic / Load Balancing section for your site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Monitors
&lt;/h3&gt;

&lt;p&gt;First, create a monitor to check the server’s status via a link. Enter a URL that is not cached and reflects the essential components of the system (e.g., database connection, working script, nginx). For my setup, I use the login page. Don’t forget to check "&lt;strong&gt;Don't verify SSL/TLS certificates (insecure)&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmws6e1nfj9gm24ab72f2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmws6e1nfj9gm24ab72f2.png" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Endpoints
&lt;/h3&gt;

&lt;p&gt;Endpoints are the connected servers. It’s essential to create one endpoint for each server: one as the primary and the other as the failover.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok996pslnbfck43iaz2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok996pslnbfck43iaz2r.png" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During Load Balancer setup, you can customize names for the Pool Name and Endpoint Name fields for convenience. Ensure the Endpoint Address fields contain the IP addresses of your primary and backup servers. In the Header Value field, enter your domain in the format domain.ltd (e.g., example.com). These settings will ensure proper traffic distribution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdwju924yzexumtmn7w6m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdwju924yzexumtmn7w6m.png" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, select the previously created monitor for server health checks. In the Health Check Regions field, specify the region where most of your audience is located to ensure health checks are performed close to users. Set the Health Threshold value to 1, meaning the pool is considered available with at least one working server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfugi8ywd0hjnqgmxue5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfugi8ywd0hjnqgmxue5.png" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Load Balancer
&lt;/h3&gt;

&lt;p&gt;Finally, consolidate everything into a unified system. Below are screenshots of the working configuration:&lt;/p&gt;

&lt;h4&gt;
  
  
  Hostname
&lt;/h4&gt;

&lt;p&gt;Enter your main domain in the format domain.ltd.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo58lcs5yadlbglodhqj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo58lcs5yadlbglodhqj.jpg" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Endpoints
&lt;/h4&gt;

&lt;p&gt;Assign your primary server to "&lt;strong&gt;Endpoints in this Load Balancer&lt;/strong&gt;" and your backup server to "&lt;strong&gt;Fallback Pool&lt;/strong&gt;".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf9z9cm3qs1lxt918mq6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf9z9cm3qs1lxt918mq6.jpg" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Monitors
&lt;/h4&gt;

&lt;p&gt;Add the previously created monitor.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcl0owbty18q4vfk0rj4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcl0owbty18q4vfk0rj4.jpg" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Traffic Steering
&lt;/h4&gt;

&lt;p&gt;Leave the default setting "&lt;strong&gt;Off: Cloudflare will route pools in failover order.&lt;/strong&gt;"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfao7jju9vjnwhqdmyct.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfao7jju9vjnwhqdmyct.jpg" alt="How to Create an Auto-Failover Load Balancer for 100% Uptime (Tutorial)" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Next at each step until you reach the Save button.&lt;/p&gt;

&lt;p&gt;Congratulations! Now you can test the setup: disable the primary server and ensure the system correctly redirects traffic to the backup server. If you have any questions, feel free to leave them in the comments—I’ll be happy to help!&lt;/p&gt;

</description>
      <category>linux</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
