<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Matej Bačo]]></title><description><![CDATA[Open-minded ​website developer sharing his knowledge.]]></description><link>https://blog.matejbaco.eu</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 00:59:48 GMT</lastBuildDate><atom:link href="https://blog.matejbaco.eu/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Found Perfect CMS after Years of Trial and Error]]></title><description><![CDATA[As a freelancer, I always looked for ways to create admin panels for my clients. It gives them control, and makes me money. It was impossible to find something simple, free, and privacy-friendly. After a fair share of experience with dozens of system...]]></description><link>https://blog.matejbaco.eu/i-found-perfect-cms-after-years-of-trial-and-error</link><guid isPermaLink="true">https://blog.matejbaco.eu/i-found-perfect-cms-after-years-of-trial-and-error</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Thu, 03 Apr 2025 11:29:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743608221667/5d946c6d-7ff0-44d1-b54a-4be9281c0404.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a freelancer, I always looked for ways to create admin panels for my clients. It gives them control, and makes me money. It was impossible to find something simple, free, and privacy-friendly. After a fair share of experience with dozens of systems, I finally found one that aligns with my expectations.</p>
<h2 id="heading-finding-the-perfect-cms">Finding the Perfect CMS</h2>
<p>Every CMS is useful. All of them exist because they have a target audience that is willing to pay, since it provides them value.</p>
<p>After years of searching, I created a set of rules that define the <em>"Perfect CMS"</em> for static sites.</p>
<ol>
<li><p><strong>Own my data</strong>. Don't store my content, and don't become a dependency for my projects.</p>
</li>
<li><p><strong>Extendable</strong>. Allow me to add custom components. Ideally open-sourced too, for ease of learning.</p>
</li>
<li><p><strong>Framework agnostic</strong>. Don't tell me to use Angular. Integrate with any framework, and without a framework.</p>
</li>
<li><p><strong>Free</strong>. Don't limit the amount of projects, clients, or features. Don't rely on paid users to keep the product alive.</p>
</li>
<li><p><strong>Self-hostable</strong>. Don't require Cloud for authentication. Don't vendor lock-in access to CMS.</p>
</li>
</ol>
<p>On top of those points, CMS must be simple to use, quick to implement, and nice-looking. Those all make it easier to convince clients they need a CMS.</p>
<h3 id="heading-choose-wisely-choose-git-based-cms">Choose Wisely, Choose Git-based CMS</h3>
<blockquote>
<p><strong>Own my data</strong>. Don't store my content, and don't become a dependency for my project.</p>
</blockquote>
<p>The biggest difference between Git-based CMS and API-based CMS is complexity. Complexity can be beneficial to large blog platforms or e-commerce platforms, but that is not a typical project of freelancers. Much more common is a landing page, personal website, or community platform.</p>
<p>Simple websites don't benefit from API-based CMS, and get all the downsides. One must use an SDK, or send raw HTTP requests to fetch the data. One must implement caching to make their application quick again. And most importantly, one must have fallback behavior if CMS ever goes into downtime.</p>
<p>Git-based CMS solves all the pain points above. All data is stored in a file defined by you, whether it's JSON, YAML, TOML, or Markdown. Data is always accessible during the build step, so importing it directly gives you access with code auto-completion, and no delays when rendering the website. Data is stored directly as part of your source code, and doesn't create any direct link with the CMS that is used to edit them.</p>
<p>A significant downside of using Git-based CMS is the speed at which changes are reflected. Since a re-build is required after each change, it can take a couple of seconds (or up to a few minutes, depending on which framework you use) to apply changes on production. This can be a bit frustrating, but a typical freelancer's client doesn't mind. If one would honestly need changes instantly reflected on a website, they are at size when they benefit from API-based CMS anyway.</p>
<p>Thankfully, there are many Git-based CMS such as <a target="_blank" href="https://decapcms.org/">Decap CMS</a>, <a target="_blank" href="https://tina.io/">TinaCMS</a>, or <a target="_blank" href="https://craftercms.com/">Crafter CMS</a>.</p>
<h3 id="heading-component-library-is-not-enough">Component Library is Not Enough</h3>
<blockquote>
<p><strong>Extendable</strong>. Allow me to add custom components. Ideally open-sourced too, for ease of learning.</p>
</blockquote>
<p>Basic components are enough for the first iteration of your CMS. As client's business grows, so do their demands. Coloring data based on importance, or adding map picker to visually represent location, may sooner or later become highly important for CMS users efficiency.</p>
<p>When that happens, your CMS should provide you with docs on how to extend it, instead of asking you to submit a feature request. Relying on maintainers is the worst outcome when it comes to custom components in CMS.</p>
<p>Many providers such as <a target="_blank" href="https://directus.io/">Directus</a>, <a target="_blank" href="https://www.appsmith.com/">Appsmith</a>, or <a target="_blank" href="https://budibase.com/">Budibase</a> all have ways to fully customize the viewing and editing experience of each field.</p>
<h3 id="heading-cms-doesnt-live-in-my-application">CMS Doesn't Live in My Application</h3>
<blockquote>
<p><strong>Framework agnostic</strong>. Don't tell me to use Angular. Integrate with any framework, and without a framework.</p>
</blockquote>
<p>You likely experienced this before. You find an amazing UI library, you decide to use it in your next project, and moments later you realize it only supports Next.js. You find an amazing PDF library to generate great-looking invoices, only to realize it's for PHP. You find ...</p>
<p>Next.js and PHP are great. But it's my decision if they are great for the project I work on. If CMS provided support only for a specific framework, either you can't use it for every project you build, or you throw yourself into a framework you may not be familiar with, or even worse, it may not be the right choice for the job. CMS is there to assist you, not to throw logs under your feet.</p>
<p><a target="_blank" href="https://payloadcms.com/">Payload</a>, a CMS powered by Next.js, or <a target="_blank" href="https://github.com/sveltia/sveltia-cms">Sveltia CMS</a>, a Decap CMS alternative using Svelte, are examples of CMS that I recommend to avoid until they become framework agnostic.</p>
<h3 id="heading-save-money-to-make-money">Save Money to Make Money</h3>
<blockquote>
<p><strong>Free</strong>. Don't limit the amount of projects, clients, or features. Don't rely on paid users to keep the product alive.</p>
</blockquote>
<p>Limits and free tiers are foundation stones of every commercial platform. It may remain open sourced, or a version 2.0 may be released with cloud-only support. It could remain free for unlimited projects, or it could change the limit to 3 projects over the weekend. It's also a matter of trust whether a Cloud doesn't change pricing from $5 per month to $15.</p>
<p>When picking a preferred CMS for simple sites, it should not limit the amount of projects, collaborators, fields, or rows. Allowing pricing to affect you eventually creates expenses, which lower your income from long-term clients.</p>
<p><a target="_blank" href="https://www.sanity.io/">Sanity</a>, <a target="_blank" href="https://www.contentful.com/">Contentful</a>, and <a target="_blank" href="https://hygraph.com/">Hygraph</a> are a few examples. Very often a CMS has a pricing page, but is also open-source, and has docs on how to self-host it for free. A great example of that is <a target="_blank" href="https://directus.io/">Directus</a>, and there is no need to avoid those CMS.</p>
<h3 id="heading-authentication-a-bait-for-headless-cms">Authentication, a Bait for Headless CMS</h3>
<blockquote>
<p><strong>Self-hostable</strong>. Don't require Cloud for authentication. Don't vendor lock-in access to CMS.</p>
</blockquote>
<p>Git-based CMS often provide Cloud authentication to simplify process of setting it up. This is perfect for development, and initial integration to ensure everything works well together. As soon as application goes to production, it's a great risk to keep using the Cloud offering.</p>
<p>Primary problem is the fact you are authorizing someone's else GitHub application, not yours. This means maintainers of the Cloud offering of the CMS can see contents of your repository, do changes, and possibly even more. This creates potential for security risks, makes app less likely to comply with strict privacy regulations, and can create helpless situation during authentication gateway downtimes. Such Cloud authentication can also sunset at any moment.</p>
<h2 id="heading-honorable-mention-outstatic">Honorable Mention: Outstatic</h2>
<p>Until now, my favourite CMS was <a target="_blank" href="https://outstatic.com/">Outstatic</a>, a CMS for Next.js applications. It's fully open source project that can be self-hosted, and focuses on keeping your data inside your git repository. It's simple-looking interface is amazing for any small CMS, and AI integration for text generation can be a great plus for some projects.</p>
<p>Downside of Outstatic is limited collaboration features, since it doesn't come with database, so only authentication method is GitHub OAuth. This can be limiting, as explaining to clients what GitHub is and why they need the account can be hard. Additionally, it's SDKs tightly couple you to use Next.js.</p>
<h2 id="heading-why-pages-cms-is-the-best-cms">Why Pages CMS is the Best CMS</h2>
<p>Considering all of the above, the best CMS is <a target="_blank" href="https://pagescms.org/">Pages CMS</a>, a modern Git-based CMS with a focus on static sites. With a single configuration file, in matter of minutes, it allows you to manage your content and its media files through an intuitive UI. Pages CMS support all collaboration features you or your client might need including MagicURL login, history of changes, and more. It comes with a responsive design to let your client do changes from their phone.</p>
<p>Pages CMS keeps all your contents inside your repository, and only requires a single configuration file to be set up. What's even more, it's framework agnostic and can be used with Astro, Next.js, SvelteKit, or any other framework. Because of the nature of Pages CMS, it doesn't integrate with a framework, and only looks at files containing your data such as JSON, YAML, Markdown, or TOML.</p>
<p>It supports 9 file formats and 13 built-in field types. While this is more than enough, Pages CMS can be forked, self-hosted, and extended with custom components. Doing this requires basic understanding of React, but detailed documentation and existing field types serve as great starting points.</p>
<p>The entirety of Pages CMS is free and open sourced, including its Cloud platform. Cloud offering has no limits on the amount of collaborators, projects, or content. Developers of Pages CMS even announced <code>Free forever</code> as part of their <a target="_blank" href="https://github.com/pages-cms/pages-cms/discussions/128">1.0 release notes</a>.</p>
<h2 id="heading-lets-integrate-with-pages-cms">Let's Integrate with Pages CMS</h2>
<p>As with any CMS I come across, I try to use it with an application. This time I decided to use <a target="_blank" href="https://github.com/alamguardin/Astrolink">Astrolink</a>, a minimal alternative to <a target="_blank" href="https://linktr.ee/">Linktree</a>. You can check it out on a <a target="_blank" href="https://astrolink.vercel.app/">demo page</a>, or see it's details in <a target="_blank" href="https://astro.build/themes/details/astrolink-template-to-share-about-yourself/">Astro theme catalog</a>.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a85jl7ml4h7ckefxpi9d.png" alt="Astrolink" /></p>
<h2 id="heading-prepare-the-website">Prepare the Website</h2>
<p>Initial observation shows Astrolink already sources all of the page data from a single file <a target="_blank" href="https://github.com/alamguardin/Astrolink/blob/main/src/data/user.json">src/data/user.json</a>. This means, Pages CMS will only need access to this file, and I don't need to make any adjustments to source code.</p>
<blockquote>
<p>If you want to use Pages CMS with your project, and you have your data inside components or JavaScript/TypeScript files, I highly recommend moving data to JSON files first. Many frameworks have ability to import JSON files directly into components. Those that don't can store JSON files in public assets folder, and be loaded with a <code>fetch()</code> request.</p>
</blockquote>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gl526m25zw06djw8k340.png" alt="User.json file" /></p>
<p>Getting Astrolink live was matter of a few very simple steps:</p>
<ol>
<li><p>Fork GitHub repository</p>
</li>
<li><p>Deploy website to Cloud (<a target="_blank" href="https://vercel.app/">Vercel</a>, <a target="_blank" href="https://www.netlify.com/">Netlify</a>, or others)</p>
</li>
<li><p>Connect custom domain using DNS</p>
</li>
</ol>
<p>Once deployed, it was time to configure Pages CMS.</p>
<h2 id="heading-connect-with-pages-cms">Connect with Pages CMS</h2>
<p>To get started with <a target="_blank" href="https://pagescms.org/">Pages CMS</a>, I visited their <a target="_blank" href="https://app.pagescms.org/">Cloud platform</a>, and signed in with GitHub. Through this process I authorized with Pages CMS OAuth application, and gave their GitHub application access to my repositories, including read and write access.</p>
<blockquote>
<p>Pages CMS can be self-hosted <strong>very easily</strong>, and they have a step-by-step guide on how to do that. I highly recommend self-hosting Pages CMS and authorizing your own GitHub application for security and privacy reasons.</p>
</blockquote>
<p>Once my GitHub was connected, I saw my newly forked Astrolink repository in the list of Pages CMS projects.</p>
<p>The project was not ready to be used yet, because Pages CMS needs a <code>.pages.yml</code> configuration file that will hold all the Pages CMS configuration. Frankly, I didn't need to know that. A nice-looking error page explained it all, and provided a button to create an empty configuration file to continue.</p>
<p>Afterwards, I spent a couple of minutes reading <a target="_blank" href="https://pagescms.org/docs/">Pages CMS documentation</a>, more specifically <a target="_blank" href="https://pagescms.org/docs/configuration/">Configuration</a> and <a target="_blank" href="https://pagescms.org/docs/examples/">Examples</a> pages.</p>
<p>While fields in YAML file were not any standard knowledge developers usually have, it was very easy to understand. I am nowhere near knowing the exact schema by heart, but I already feel confident setting up a configuration file for any kind of data thanks to its highlighting, and schema validation directly in Pages CMS.</p>
<p>Below you can find configuration file I used for the Astrolink project:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">content:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">user</span>
    <span class="hljs-attr">label:</span> <span class="hljs-string">User</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">src/data/user.json</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">file</span>
    <span class="hljs-attr">fields:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">name</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">profession</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">links</span>
    <span class="hljs-attr">label:</span> <span class="hljs-string">Links</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">src/data/user.json</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">file</span>
    <span class="hljs-attr">fields:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">links</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">object</span>
        <span class="hljs-attr">list:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">fields:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">title</span>
           <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">description</span>
           <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">icon</span>
           <span class="hljs-attr">description:</span> <span class="hljs-string">Icon</span> <span class="hljs-string">names</span> <span class="hljs-string">can</span> <span class="hljs-string">be</span> <span class="hljs-string">found</span> <span class="hljs-string">at</span> <span class="hljs-string">www.remixicon.com</span>
           <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">url</span>
           <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
           <span class="hljs-attr">pattern:</span> <span class="hljs-string">^(https?:\/\/)?(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$</span>
</code></pre>
<p>As I saved the configuration file, new options appeared in the left side menu. Inside, I found my schema applied in a simple interface of inputs and buttons. What's even more interesting, all data was already loaded, indicating the connection to data stored in <code>src/data/user.json</code> was successful.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbikndmz267nv75bccqn.png" alt="Pages CMS UI" /></p>
<p>I updated a few texts, and saved the changes. A commit was created automatically, pushed, and a Vercel deployment started. After couple of seconds, I refreshed my website and changes I made were live.</p>
<h2 id="heading-collaboration-with-client">Collaboration with Client</h2>
<p>Since the main purpose of CMS is to provide the client with a simple interface to update content on their static site, I decided to try the full experience with Pages CMS.</p>
<p>In collaboration settings, I added a friend of mine to the project through email invitation. They received the invitation in their inbox, and followed steps provided. An amazing feature presented itself when I noticed the sign-in page supports passwordless email login alongside GitHub OAuth. I can't stress enough how important this is since clients almost never have GitHub accounts.</p>
<p>After a passwordless login using Magic URL, my friend already saw the Astrolink project in their list, and started editing content. No setup was needed from his side.</p>
<p>Finally, yet another feature amazed me. When my friend started making changes, I saw it all on the right side of Pages CMS, which served as a history of changes. If a client <strong>ever</strong> does a change that breaks the website, as a developer I can easily see what the change was, when, and within a few clicks I can rollback the changes to make the website stable again. Moments like this make business relations more stable and lucrative.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m15umo1vfmre6wi5ebbz.png" alt="Pages CMS history" /></p>
<p>Both website and CMS are now live! Within less than hour of finding out about Pages CMS, I was able to deploy my first site with CMS integrated. You can visit my <a target="_blank" href="https://matejbaco.vercel.app/">Astrolink website</a>, and I invite you to make yours to try out Pages CMS. Additionally, I recommend you to make picture URL on the website configurable, to gain further experience with Pages CMS. For those looking for challenge, Pages CMS supports media, so you can switch from picture URL to proper drag&amp;drop file input.</p>
<h2 id="heading-pages-cms-in-production">Pages CMS in Production</h2>
<p>As mentioned above, Pages CMS requires read and write access to your repository, which can be scary for multiple reasons:</p>
<ul>
<li><p>Security: Pages CMS Cloud has full access to your repository, possibly deleting the entire project.</p>
</li>
<li><p>Privacy: Pages CMS Cloud has permission to see the source code of your application.</p>
</li>
<li><p>Billing: Pages CMS Cloud can become paid. They promised it won't, but it can.</p>
</li>
</ul>
<p>While self-hosting doesn't fully address security, it surely makes it more secure. Bugs in code can still cause problems, but the risk is much more manageable.</p>
<p>Setting up self-hosted Pages CMS looks like complex task since it has many moving parts, but their docs are <strong>amazing</strong> and provide follow-along instructions to set everything up. The entire setup was made with developers in mind, as it recommends <a target="_blank" href="https://resend.com/">Resend</a> and <a target="_blank" href="https://turso.tech/">Turso</a>, both Cloud applications that provide free tiers.</p>
<p>What caught me by surprise was how easy it is to add custom components. From my past experience building CMS for clients, I know simple component changes can make a big difference. <em>For example, simply coloring a date red or green depending on package shipping status can avoid delayed deliveries.</em> Not only was the process of adding custom components documented, it also linked to over a dozen existing components providing a starting point.</p>
<blockquote>
<p>If you are interested to gain confidence with custom components, I recommend to build component that stores latitude and longitude of location on a map, and provide interface using <a target="_blank" href="https://developers.google.com/maps">Google Maps</a>, <a target="_blank" href="https://www.openstreetmap.org/">OpenStreetMap</a>, or similar map provider.</p>
</blockquote>
<p>If you use Pages CMS in production, and successfully self-hosted the application, it's the best time to consider <a target="_blank" href="https://github.com/sponsors/hunvreus">sponsoring Pages CMS</a>. Engineers behind this amazing open-source project are active, and providing them with financial support allows them to develop new features, which can benefit you in the long run.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I am finally at peace. No more searching for a better CMS. Pages CMS is my go-to, if I ever need one for sites I build. It provides a nice-looking and simple interface that's quick and easy to setup. It's free, open-source, self-hostable, framework-agnostic, and doesn't lack collaboration features. What more should I ask for.</p>
<p>If you don't share my passion for Pages CMS, take inspiration from my criteria for the perfect CMS. Many of them make sense in every situation, and help you find a solution that is long-term, and sustainable. If it wasn't clear already, every static site deserves a CMS.</p>
]]></content:encoded></item><item><title><![CDATA[Auth UI, Universal Auth Service]]></title><description><![CDATA[Auth UI: Appwrite Hashnode Hackathon
🙌 Team Details

Matej Bačo - @_meldiron

🎶 Oh, it's just me, myself and I 🎶
ℹ️ Description of Project
Auth UI is a fully customizable login flow for your applications. Using Auth UI gives you a ready-made UI fo...]]></description><link>https://blog.matejbaco.eu/auth-ui-universal-auth-service</link><guid isPermaLink="true">https://blog.matejbaco.eu/auth-ui-universal-auth-service</guid><category><![CDATA[Appwrite Hackathon]]></category><category><![CDATA[Appwrite]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Fri, 19 May 2023 19:28:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684521865611/e9685fd8-0968-4e98-aea8-c98000be52a3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://www.authui.site/">Auth UI</a>: <a target="_blank" href="https://appwrite.io">Appwrite</a> <a target="_blank" href="https://hashnode.com">Hashnode</a> Hackathon</p>
<h2 id="heading-team-details">🙌 Team Details</h2>
<ul>
<li>Matej Bačo - <a target="_blank" href="https://twitter.com/_meldiron">@_meldiron</a></li>
</ul>
<p>🎶 Oh, it's just me, myself and I 🎶</p>
<h2 id="heading-description-of-project">ℹ️ Description of Project</h2>
<p>Auth UI is a fully customizable login flow for your applications. Using Auth UI gives you a ready-made UI for all key features of the Authorization Server. Thanks to the adapter approach, you can connect Auth UI to any backend of your choice, such as Appwrite.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684523379107/8c3f5fdb-b1c9-4ba2-8d58-638ab4e62872.png" alt class="image--center mx-auto" /></p>
<p>When a user needs to authenticate, your application redirects the user to Auth UI, where the user can pick one of many options to sign in. Not only that, users can also sign up, recover passwords or even manage their accounts.</p>
<p>When using Auth UI, you don't need to make any application-level changes. You can simply add one redirect to Auth UI page, and we will handle the rest. Your applications will also benefit from any improvements we make to Auth UI without the need for you to change your code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684523497816/d63668b7-c10e-41cd-b05c-1e2b876cc58f.png" alt class="image--center mx-auto" /></p>
<p>Auth UI was made to take place in your existing applications with ease. With configurable redirect URLs, you can set up and integrate Auth UI within a few minutes.</p>
<p>Auth UI page is configurable and lets you design it, decide on the domain name, and select which sign-in methods are allowed. By customizing brand color, company logo, and even border roundness, you can make the authentication page look and feel almost exactly the same as the rest of your application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684523822453/22dfa6b1-2865-49f5-9b1a-a57e7ec840fa.png" alt class="image--center mx-auto" /></p>
<p>Sadly, Auth UI is not a one-stop shop. Some applications need more complex auth flow or even fully customizable authentication pages. If that's your case, you might be better off implementing everything on your own. Auth UI was created to simplify the creation of web apps and take care of the most repetitive pages.</p>
<p>And that's not all! The future of Auth UI is bright and includes some exciting features such as new providers, new sign-in methods, and more.</p>
<h2 id="heading-tech-stack">🧰 Tech Stack</h2>
<p>Technologies:</p>
<ul>
<li><p><a target="_blank" href="https://appwrite.io/">Appwrite</a> | Backend taking care of Authentication, Databases, and File Storage 🗃️</p>
</li>
<li><p><a target="_blank" href="https://kit.svelte.dev/">Svelte Kit</a> | Rendering framework to help with SSR, SEO, and routing 🤖</p>
</li>
<li><p><a target="_blank" href="https://svelte.dev/">Svelte</a> | JavaScript framework to make the website dynamic and quick ⚡</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a> | Programming language to keep website strongly typed and stable for production</p>
</li>
<li><p><a target="_blank" href="https://pink.appwrite.io/">Pink Design</a> | UI framework making websites look nice by default ✨</p>
</li>
</ul>
<p>Production:</p>
<ul>
<li><p><a target="_blank" href="https://cloud.appwrite.io/">Appwrite Cloud</a> | Hosted solution of Appwrite</p>
</li>
<li><p><a target="_blank" href="https://vercel.com/meldiron">Vercel</a> | Website hosting provider</p>
</li>
</ul>
<h2 id="heading-challenges-i-faced">💢 Challenges I Faced</h2>
<p>Like every (good) developer, I was finding lazy ways to do stuff, not the good ones 😅 Since I needed the ability to edit an existing page, I added an extra section when creating a page called <code>Master Password</code>. Here, a developer would enter his password, which will later be necessary later, to delete or edit their Auth UI page. Only when I was done with the implementation I noticed passwords were saved unhashed, unprotected, and this whole idea was not the proudest moment of my creativity 🤦</p>
<p>A solution to this was actually pretty crazy. I needed authentication pages for Auth UI, so I thought hmmmm, why not use Auth UI for that? 🤔 Now authentication into Auth UI is powered by Auth UI itself. Eat your own dog food! 😄</p>
<p>Thankfully the rest of the journey was pretty straightforward. I am yet to face production on fire, bugs in the core of the application, and database migrations deleting all the pages 🤪 Let's see where future takes Auth UI 😇</p>
<h2 id="heading-public-code-repo">🔗 Public Code Repo</h2>
<p>GitHub: <a target="_blank" href="https://github.com/Meldiron/authui">meldiron/authui</a></p>
<h2 id="heading-demo-link">⚡ Demo Link</h2>
<p>Live Demo: <a target="_blank" href="https://www.authui.site/">authui.site</a></p>
]]></content:encoded></item><item><title><![CDATA[Go Limitless with New Appwrite Queries]]></title><description><![CDATA[Appwrite is an open-source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, ...]]></description><link>https://blog.matejbaco.eu/go-limitless-with-new-appwrite-queries</link><guid isPermaLink="true">https://blog.matejbaco.eu/go-limitless-with-new-appwrite-queries</guid><category><![CDATA[Open Source]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Fri, 23 Sep 2022 09:31:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1663925435717/oebW4avxE.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://appwrite.io/">Appwrite</a> is an open-source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, real-time databases, cloud functions, webhooks, and much more!</p>
<p>What’s even more amazing is that we recently became number 1! 🥳 Long-awaited 1.0 release brings amazing features to make web and app development even more <strong>FUN</strong>. One of them being a new query syntax that brings numerous possibilities, but first, let’s understand what it means to <code>query</code>.</p>
<h2 id="heading-what-is-a-query">🔍 What Is a Query?</h2>
<p>The database is a critical part of any application. It’s a place where all the information generated by users is stored. Storing data is important but only useful if you can <strong>read the data</strong>. In the end, who would use an app where they can create a profile but never look at it? 🤔</p>
<p>It’s as simple as that! Querying is the process of reading the data of your application. Queries can be as simple as “Give me all you got”, but can also get robust with many conditions, sorting operations, or pagination.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8mb9r3pzrpdo91lbpl7.jpg" alt="Show me what you got meme" /></p>
<p>Let’s see a query in action! For this example, I will use SQL (Structured <strong>Query</strong> Language), which lets us read data from a database by writing a human-like sentence.</p>
<p>Let’s start with a simple query to get all JavaScript frameworks in the world:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> frameworks;
</code></pre>
<p>This query could serve all of our needs but would get extremely slow as our database starts to grow. Let’s improve the query by filtering the results and only get popular frameworks:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> frameworks <span class="hljs-keyword">WHERE</span> popularity &gt; <span class="hljs-number">0.9</span>;
</code></pre>
<p>We might still be getting thousands of results, but we never show that on the website, right? Let’s only get the first 10 results:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> frameworks <span class="hljs-keyword">WHERE</span> popularity &gt; <span class="hljs-number">0.9</span> <span class="hljs-keyword">LIMIT</span> <span class="hljs-number">10</span>;
</code></pre>
<p>Finally, let’s make sure to show the most popular frameworks first:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> frameworks <span class="hljs-keyword">WHERE</span> popularity &gt; <span class="hljs-number">0.9</span> <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> popularity <span class="hljs-keyword">DESC</span> <span class="hljs-keyword">LIMIT</span> <span class="hljs-number">10</span>;
</code></pre>
<p>We just built a basic query! 💪 Let’s now take this knowledge and build some queries that we can use to read data from Appwrite Database.</p>
<h2 id="heading-appwrite-query-syntax">🖋️ Appwrite Query Syntax</h2>
<p>Let's start by taking a look at <strong>filter queries</strong>. These queries are meant to reduce the amount of results by checking if a condition is met regarding each document. Appwrite supports many filter queries that let you compare against a word, text, number or booleans. Following is a table of all available filter queries and an example of how such a query could look like in SQL world to make understanding easier:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Appwrite Query</td><td>SQL Query</td></tr>
</thead>
<tbody>
<tr>
<td>Query.equal("role", "Developer")</td><td>WHERE role = 'Developer'</td></tr>
<tr>
<td>Query.equal("role", [ "Developer", "Designer" ])</td><td>WHERE role = 'Developer' OR role = 'Designer'</td></tr>
<tr>
<td>Query.notEqual("category", "Flutter")</td><td>WHERE category != "Flutter"</td></tr>
<tr>
<td>Query.lessThan("age", 100)</td><td>WHERE age &lt; 100</td></tr>
<tr>
<td>Query.lessThanEqual("age", 100)</td><td>WHERE age &lt;= 100</td></tr>
<tr>
<td>Query.greaterThan("balance", 4.99)</td><td>WHERE balance &gt; 4.99</td></tr>
<tr>
<td>Query.greaterThanEqual("balance", 4.99)</td><td>WHERE balance &gt;= 4.99</td></tr>
<tr>
<td>Query.search("content", "phone number")</td><td>WHERE MATCH(content) AGAINST('phone number' IN BOOLEAN MODE)</td></tr>
</tbody>
</table>
</div><p>Next you can take advantage of <strong>sort queries</strong> that lets you order results of a query in a specific way. You can see this feature on all eshops that let you sort products by popularity, price or reviews. Following queries are available in Appwrite:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Appwrite Query</td><td>SQL Query</td></tr>
</thead>
<tbody>
<tr>
<td>Query.orderAsc("price")</td><td>ORDER BY price ASC</td></tr>
<tr>
<td>Query.orderDesc("$createdAt")</td><td>ORDER BY createdAt DESC</td></tr>
</tbody>
</table>
</div><p>Last but not least, you can write <strong>pagination queries</strong>. There are different ways of paginating over your database that you can learn more in our <a target="_blank" href="https://dev.to/appwrite/this-is-why-you-should-use-cursor-pagination-4nh5">Database Paginations</a> article. In short, we could do offset pagination using limit and offset queries, or cursor pagination using limit and cursor. All of that is possible in Appwrite using following queries:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Appwrite Query</td><td>SQL Query</td></tr>
</thead>
<tbody>
<tr>
<td>Query.limit(10)</td><td>LIMIT 10</td></tr>
<tr>
<td>Query.offset(30)</td><td>OFFSET 30</td></tr>
<tr>
<td>Query.cursorAfter("documentId15")</td><td>WHERE id &gt; 15</td></tr>
<tr>
<td>Query.cursorBefore("documentId50")</td><td>WHERE id &lt; 50</td></tr>
</tbody>
</table>
</div><h2 id="heading-querying-any-appwrite-service">🧰 Querying Any Appwrite Service</h2>
<p>If you used Appwrite before, you might have noticed that all of the above features were already available, just in a different syntax. So… Why the change?</p>
<p><strong>✨ Consistency ✨</strong></p>
<p>These features were only available in some services! 😦 Our mission was to implement queries into all list methods (where it makes sense), but that would be pain for you to learn, and pain for us to maintain. Thanks to Queries syntax Appwrite now offers a consistent interface to query <strong>any</strong> resource in an exactly the same way.  Let’s see it in action!</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Client, Databases, Query } <span class="hljs-keyword">from</span> <span class="hljs-string">'appwrite'</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client();
client
    .setEndpoint(<span class="hljs-string">'https://[HOSTNAME_OR_IP]/v1'</span>)
    .setProject(<span class="hljs-string">'PROJECT_ID]'</span>);
<span class="hljs-keyword">const</span> database = <span class="hljs-keyword">new</span> Databases(client);

<span class="hljs-comment">// Get products from eshop database that are published and cost below 50$. Ordered by price to get most expensive first on third page, getting 10 items per page</span>
<span class="hljs-keyword">const</span> productsList = <span class="hljs-keyword">await</span> database.listDocuments(<span class="hljs-string">'eshop'</span>, <span class="hljs-string">'products'</span>, [
    Query.equal(<span class="hljs-string">"published"</span>, <span class="hljs-literal">true</span>),
    Query.lessThan(<span class="hljs-string">"price"</span>, <span class="hljs-number">49.99</span>),
    Query.limit(<span class="hljs-number">10</span>),
    Query.offset(<span class="hljs-number">20</span>),
    Query.orderDesc(<span class="hljs-string">"price"</span>)
]);

<span class="hljs-comment">// Get up to 50 disabled collections in eshop database</span>
<span class="hljs-keyword">const</span> collectionsList = <span class="hljs-keyword">await</span> database.listCollections(<span class="hljs-string">'eshop'</span>, [
    Query.equal(<span class="hljs-string">"enabled"</span>, <span class="hljs-literal">false</span>),
    Query.limit(<span class="hljs-number">50</span>)
]);

<span class="hljs-comment">// Get database by name</span>
<span class="hljs-keyword">const</span> databasesList = <span class="hljs-keyword">await</span> database.list([
    Query.equal(<span class="hljs-string">"name"</span>, <span class="hljs-string">'eshop'</span>),
    Query.limit(<span class="hljs-number">1</span>)
]);
</code></pre>
<p>Let’s see a few more examples with different services 😇</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> storage = <span class="hljs-keyword">new</span> Storage(client);

<span class="hljs-comment">// Get all JPEG or PNG files ordered by file size</span>
<span class="hljs-keyword">const</span> filesList = <span class="hljs-keyword">await</span> storage.listFiles(<span class="hljs-string">'productPhotos'</span>, [
    Query.equal(<span class="hljs-string">"mimeType"</span>, [ <span class="hljs-string">"image/jpeg"</span>, <span class="hljs-string">"image/png"</span> ]),
    Query.orderAsc(<span class="hljs-string">"sizeActual"</span>)
]);
</code></pre>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> functions = <span class="hljs-keyword">new</span> Functions(client);

<span class="hljs-comment">// Get failed executions that were triggered by http request and lasted at least 5 seconds</span>
<span class="hljs-keyword">const</span> executionsList = <span class="hljs-keyword">await</span> functions.listExecutions(<span class="hljs-string">'createOrder'</span>, [
    Query.equal(<span class="hljs-string">"status"</span>, <span class="hljs-string">"failed"</span>),
    Query.equal(<span class="hljs-string">"trigger"</span>, <span class="hljs-string">"http"</span>),
    Query.greaterThanEqual(<span class="hljs-string">"time"</span>, <span class="hljs-number">5</span>) <span class="hljs-comment">// in seconds</span>
]);
</code></pre>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> teams = <span class="hljs-keyword">new</span> Teams(client);

<span class="hljs-comment">// Get 5th biggest team that has at least 100 members</span>
<span class="hljs-keyword">const</span> teamsList = <span class="hljs-keyword">await</span> teams.list([
    Query.greaterThan(<span class="hljs-string">"total"</span>, <span class="hljs-number">100</span>),
    Query.orderDesc(<span class="hljs-string">"total"</span>),
    Query.limit(<span class="hljs-number">1</span>),
    Query.offset(<span class="hljs-number">5</span>)
]);
</code></pre>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> Users(client);

<span class="hljs-comment">// Find all Johns that verified their email address</span>
<span class="hljs-keyword">const</span> usersList = <span class="hljs-keyword">await</span> users.list([
    Query.equal(<span class="hljs-string">"emailVerification"</span>, <span class="hljs-literal">true</span>),
    Query.search(<span class="hljs-string">"name"</span>, <span class="hljs-string">"John"</span>)
]);
</code></pre>
<p>As you can see, possibilities are limitless! 🤯 What’s more, the new syntax opens doors for new exciting features such as join, specific selects, subqueries, and new operators. We will continue working and making the Appwrite database more flexible and closer to the under the hood database being used, whether it’s MySQL, MariaDB, or in the future, MongoDB.</p>
<h2 id="heading-conclusion">👨‍🎓 Conclusion</h2>
<p>New Appwrite queries syntax introduced in 1.0 brings long-awaited consistency across all Appwrite services. This not only eases a learning curve of Appwrite, but also introduces new possibilities to allow you to create even more amazing applications!</p>
<h2 id="heading-learn-more">📚 Learn more</h2>
<p>You can use the following resources to learn more and get help:</p>
<ul>
<li>🚀 <a target="_blank" href="https://github.com/appwrite">Appwrite Github</a></li>
<li>📜 <a target="_blank" href="https://appwrite.io/docs">Appwrite Docs</a></li>
<li>💬 <a target="_blank" href="https://appwrite.io/discord">Discord Community</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Appwrite Loves Open Source: Why I Chose To Sponsor Offen]]></title><description><![CDATA[Open-source is at the ❤️ of everything we do at Appwrite, and we want to enable and foster the open-source community that helped us grow to thrilling 24,000 stars on GitHub. Open-source projects require a great deal of effort to maintain and grow. We...]]></description><link>https://blog.matejbaco.eu/appwrite-loves-open-source-why-i-chose-to-sponsor-offen</link><guid isPermaLink="true">https://blog.matejbaco.eu/appwrite-loves-open-source-why-i-chose-to-sponsor-offen</guid><category><![CDATA[Open Source]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Sat, 17 Sep 2022 06:35:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1663396401549/9ALc31p5w.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Open-source is at the ❤️ of everything we do at Appwrite, and we want to enable and foster the open-source community that helped us grow to thrilling 24,000 stars on <a target="_blank" href="https://github.com/appwrite/appwrite">GitHub</a>. Open-source projects require a great deal of effort to maintain and grow. We use open-source tools every day to build Appwrite, and we want to help our community. To give back, each Appwrite engineer gets to pick an open-source project for Appwrite to sponsor for one year.</p>
<h2 id="heading-fair-analytics-tool">👁️ Fair analytics tool</h2>
<p>Offen’s main product is <a target="_blank" href="https://www.offen.dev/">web analytics</a>, which is quite unique if you ask me. The analytics tool is a core requirement of any commercial website out there, as it provides many KPIs to define marketing success. Not only that, analytics can help you target your services to the right clients, increasing your overall income.</p>
<p>There are many (even self-hosted) analytics tools, but Offen is unique enough to beat them all in my eyes. While others focused on collecting valuable information and storing them lawfully, Offen’s core value is to create visitor-friendly analytics. At your first interaction with Offen, you will notice it only allows first-party cookies, which is a great example of how they focus on visitor security in exchange for making setup a bit more complex. Once you set it up, you notice a second important feature, you can’t really see visitors in your dashboard yet. Offen is opt-in only, which means, you don’t track visitor until he clicks ‘Allow’. Offen also comes with a dashboard for visitors to see what <strong>exact</strong> information you have about them, with an option to opt-out or delete all data with 1 click.</p>
<p>Alongside these product-defining features, you can see functionality that can easily compare with industry-leading competitors:</p>
<ul>
<li>Customizable consent banner</li>
<li>Comply with GDPR</li>
<li>End-to-end encryption</li>
<li>Much more!</li>
</ul>
<p>I am yet to use Offen analytics tool on a real project, but since day one I learned about it, I have been recommending it as a Plausible and Google Analytics alternative.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90iao220m9eq9i2inrxu.png" alt="Offen showcase" /></p>
<h2 id="heading-developer-tooling">🧰 Developer tooling</h2>
<p>Alongside the amazing analytics project Offen has to provide, their GitHub organization includes many different tools such as <a target="_blank" href="https://github.com/offen/schemaify">schemaify</a>, <a target="_blank" href="https://github.com/offen/analyticstxt">analyticstxt</a> or <a target="_blank" href="https://github.com/offen/l10nify">l10nify</a>. The one tool I like the most is <a target="_blank" href="https://github.com/offen/docker-volume-backup">docker-volume-backup</a>.</p>
<p>Nowadays many self-hosted projects provide Docker support to ensure simple setup, upgrade, deployment and maintenance. Docker also often solves the legendary quote ‘It works on my machine’. Once you start using Docker on production (which you can and should), you will notice there is no backup&amp;restore mechanism available by default.</p>
<p>Offen provides a Docker image that you can run alongside your Docker application to back up anything you might need. Under the hood, the Offen container mounts to the exact same volumes your application does, and backups files depending on your configuration.</p>
<p>What’s cool is Offen’s Docker Volume Backup can be as simple or as complex as your project needs. On my server, I started with a simple <code>backup.sh</code> script, but gradually improved it to a more mature solution with many features Offen has to provide:</p>
<ul>
<li>Recurring backups (daily/weekly/monthly)</li>
<li>Upload backups to the cloud</li>
<li>Mirrored backups to multiple storage providers</li>
<li>Maintenance mode during backup to ensure data integrity</li>
<li>Alerts about failed backups</li>
<li>Backup encryption</li>
<li>Backup rotation to remove old ones</li>
</ul>
<p>With such advanced backup in place, all worries about data loss have been taken from my shoulder. I am so confident with Offen’s backup tool I have been recommending it to the Appwrite community to properly <a target="_blank" href="https://gist.github.com/Meldiron/47b5851663668102a676aff43c6341f7">backup their Appwrite instance</a>!</p>
<h2 id="heading-open-source-software-oss-is-hard">☢️ Open-source Software (OSS) Is Hard</h2>
<p>Since Appwrite is open-source, we understand the challenges that OSS projects face. If you fall in love ❤️ with an open-source project (like we have), consider checking out ways to contribute. Most OSS projects happily accept contributions in their own way, whether they be in the form of commits, bug 🐛 reports, advocacy, or even monetary 💰 support. If you love <code>Offen</code>, consider joining us as a sponsor. Or, if you're interested in contributing to Appwrite, check out our <a target="_blank" href="https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md">contribution guide</a>.</p>
<h2 id="heading-learn-more-about-appwrite">🔗 Learn more about Appwrite</h2>
<p>Check out Appwrite as the backend for your next <a target="_blank" href="https://appwrite.io/docs/getting-started-for-web">Web</a>, <a target="_blank" href="https://appwrite.io/docs/getting-started-for-flutter">Flutter</a>, or <a target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Server</a> application. Here are some handy links for more information:</p>
<ul>
<li><a target="_blank" href="https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md">Appwrite Contribution Guide</a></li>
<li><a target="_blank" href="https://appwrite.io/discord">Appwrite Discord</a></li>
<li><a target="_blank" href="https://github.com/appwrite">Appwrite Github</a></li>
<li><a target="_blank" href="https://appwrite.io/docs">Appwrite Documentation</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Appwrite Hand-In-Hand with Svelte Kit (SSR)]]></title><description><![CDATA[If you are here only to see how to make SSR work, you can jump into ⚡ Server Side Rendering and 🚀 Deployment sections. They explain it all. You can also check out the GitHub repository.

Let's build a project!
In this article, we will build Almost C...]]></description><link>https://blog.matejbaco.eu/appwrite-hand-in-hand-with-svelte-kit-ssr</link><guid isPermaLink="true">https://blog.matejbaco.eu/appwrite-hand-in-hand-with-svelte-kit-ssr</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Svelte]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Fri, 24 Jun 2022 18:44:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1656096048218/x7sA5UBw8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>If you are here only to see how to make SSR work, you can jump into <a class="post-section-overview" href="#ssr">⚡ Server Side Rendering</a> and <a class="post-section-overview" href="#deployment">🚀 Deployment</a> sections. They explain it all. You can also check out the <a target="_blank" href="https://github.com/Meldiron/almost-casino">GitHub repository</a>.</em></p>
<hr />
<p><strong>Let's build a project!</strong></p>
<p>In this article, we will build <strong>Almost Casino</strong>, a web application that allows you to sign in and play coin flip with virtual currency called <strong>Almost Dollar</strong>.</p>
<p>As you can see, not the brightest idea... The point of the project is not to build a 1B$ company in a weekend, instead, to showcase how we can achieve server-side rendering with <a target="_blank" href="https://appwrite.io/">Appwrite</a> backend.</p>
<h2 id="heading-pics-pics-pics">🖼️ Pics, pics, pics!</h2>
<p>Let's see what we are going to build 😎</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656096084903/A_66QpytG.png" alt="Almost Casino showcase" /></p>
<p>A single page that shows the ID of the currently logged-in user, his current balance, and a coin flip game. When playing coin flip, you can configure a bet and pick which side of the coin you would like to bet on. For the sick of simplicity, there will be no coin-flipping animation. Do you know what that means? 👀 <strong>YOU</strong> can add the cool-looking animation!</p>
<h2 id="heading-requirements">📃 Requirements</h2>
<p>We will be using multiple technologies in this demo application, and having a basic understanding of them will be extremely helpful.</p>
<p>Regarding the JavaScript framework, we will be using a simple and fun framework <a target="_blank" href="https://svelte.dev/">Svelte</a>. To allow routing and introduce better folder structure as well as the possibility of SSG and SSR, we are going to use <a target="_blank" href="https://kit.svelte.dev/">Svelte Kit</a>.</p>
<p>To give our application some cool styles, we will use <a target="_blank" href="https://tailwindcss.com/">TailwindCSS</a>, a CSS library for rapid designing.</p>
<p>Instead of writing our own backend from scratch, we will be using backend-as-a-service <a target="_blank" href="https://appwrite.io/">Appwrite</a> to build and deploy server logic easily.</p>
<p>Last but not least, we will use <a target="_blank" href="https://vercel.com/">Vercel</a> as our static.</p>
<h2 id="heading-create-svekte-kit-project">🛠️ Create Svekte Kit Project</h2>
<p>The whole Almost Casino application is open-sourced and can be found in <a target="_blank" href="https://github.com/Meldiron/almost-casino">GitHub repository</a>. The app is also live on the <a target="_blank" href="https://app.almost-casino.matejbaco.eu/">app.almost-casino.matejbaco.eu</a>.</p>
<p>For those who follow along, let's create a new Svelte Kit project:</p>
<pre><code class="lang-bash">$ npm init svelte almost-casino
</code></pre>
<pre><code class="lang-bash">✔ Which Svelte app template? › Skeleton project
✔ Add <span class="hljs-built_in">type</span> checking? › TypeScript
✔ Add ESLint <span class="hljs-keyword">for</span> code linting? … No / Yes
✔ Add Prettier <span class="hljs-keyword">for</span> code formatting? … No / Yes
✔ Add Playwright <span class="hljs-keyword">for</span> browser testing? … No / Yes
</code></pre>
<p>Next, let's enter our new project and run the development server:</p>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> almost-casino
$ npm install
$ npm run dev
</code></pre>
<p>We now have the Svelte Kit web application running and listening on <code>localhost:3000</code> 🥳</p>
<p>Let's make sure to follow <a target="_blank" href="https://tailwindcss.com/docs/guides/sveltekit">Tailwind installation</a> instructions to install it into our newly created Svelte Kit project.</p>
<h2 id="heading-appwrite-backend-setup">Appwrite backend setup</h2>
<p>If you don't have the Appwrite server running yet, please follow <a target="_blank" href="https://appwrite.io/docs/installation">Appwrite installation</a> instructions.</p>
<p>Once the server is ready, let's create an account and a project with the custom ID <code>almostCasino</code>. Next, we go into the <code>Database</code> section and create a collection with ID <code>profiles</code>. In the setting of this collection, we set <code>collection-level</code> permission with read access to <code>role:member</code>, and write access empty. Finally, we add the float attribute <code>balance</code> and mark it as required.</p>
<h2 id="heading-appwrite-service">🤖 Appwrite Service</h2>
<p>Let's start by creating a <code>.env</code> file and putting information about our Appwrite project in there:</p>
<pre><code><span class="hljs-attr">VITE_APPWRITE_ENDPOINT</span>=http://localhost/v1
<span class="hljs-attr">VITE_APPWRITE_PROJECT_ID</span>=almostCasi<span class="hljs-literal">no</span>
</code></pre><p>Before coding the application, let's create a class that will serve as an interface for all communication with our Appwrite backend. Here we will have methods for authentication, managing profile, and playing the coin flip game. Let's create new file <code>src/lib/appwrite.ts</code> with all of the methods:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { Appwrite, <span class="hljs-keyword">type</span> RealtimeResponseEvent, <span class="hljs-keyword">type</span> Models } <span class="hljs-keyword">from</span> <span class="hljs-string">'appwrite'</span>;

<span class="hljs-keyword">const</span> appwrite = <span class="hljs-keyword">new</span> Appwrite();
appwrite
  .setEndpoint(<span class="hljs-keyword">import</span>.meta.env.VITE_APPWRITE_ENDPOINT)
  .setProject(<span class="hljs-keyword">import</span>.meta.env.VITE_APPWRITE_PROJECT_ID);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Profile = {
  balance: <span class="hljs-built_in">number</span>;
} &amp; Models.Document;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppwriteService {
  <span class="hljs-comment">// SSR related</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> setSSR(cookieStr: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> authCookies: <span class="hljs-built_in">any</span> = {};
    authCookies[<span class="hljs-string">`a_session_<span class="hljs-subst">${<span class="hljs-keyword">import</span>.meta.env.VITE_APPWRITE_PROJECT_ID}</span>`</span>] = cookieStr;
    appwrite.headers[<span class="hljs-string">'X-Fallback-Cookies'</span>] = <span class="hljs-built_in">JSON</span>.stringify(authCookies);
  }

  <span class="hljs-comment">// Authentication-related</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> createAccount() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> appwrite.account.createAnonymousSession();
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getAccount() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> appwrite.account.get();
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> signOut() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> appwrite.account.deleteSession(<span class="hljs-string">'current'</span>);
  }

  <span class="hljs-comment">// Profile-related</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getProfile(userId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Profile&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> appwrite.functions.createExecution(<span class="hljs-string">'createProfile'</span>, <span class="hljs-literal">undefined</span>, <span class="hljs-literal">false</span>);
    <span class="hljs-keyword">if</span> (response.statusCode !== <span class="hljs-number">200</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(response.stderr); }

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(response.response).profile;
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> subscribeProfile(userId: <span class="hljs-built_in">string</span>, callback: <span class="hljs-function">(<span class="hljs-params">payload: RealtimeResponseEvent&lt;Profile&gt;</span>) =&gt;</span> <span class="hljs-built_in">void</span>) {
    appwrite.subscribe(<span class="hljs-string">`collections.profiles.documents.<span class="hljs-subst">${userId}</span>`</span>, callback);
  }

  <span class="hljs-comment">// Game-related</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> bet(betPrice: <span class="hljs-built_in">number</span>, betSide: <span class="hljs-string">'tails'</span> | <span class="hljs-string">'heads'</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">boolean</span>&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> appwrite.functions.createExecution(<span class="hljs-string">'placeBet'</span>, <span class="hljs-built_in">JSON</span>.stringify({
      betPrice,
      betSide
    }), <span class="hljs-literal">false</span>);

    <span class="hljs-keyword">if</span> (response.statusCode !== <span class="hljs-number">200</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(response.stderr); }

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(response.response).didWin;
  }
}
</code></pre>
<p>With this in place, we have everything ready to have proper communication between our Svelte Kit application and our Appwrite backend 💪</p>
<blockquote>
<p>You can notice we execute some functions in this class, for instance, <code>createProfile</code> or <code>placeBet</code>. We use Appwrite Functions for these actions to keep them secure and not allow clients to make direct changes to their money balance. You can find the source code of these functions in <a target="_blank" href="https://github.com/Meldiron/almost-casino/tree/master/functions">GitHub repository</a>.</p>
</blockquote>
<h2 id="heading-authentication-page">🔐 Authentication Page</h2>
<p>We start by creating a button to create an account. Since we are going to be using anonymous accounts, we don't need to ask visitor for any information:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">on:click</span>=<span class="hljs-string">{onRegister}</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center space-x-3 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"</span>
  &gt;</span>
    {#if registering}
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    {/if}
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Create Anonymous Account<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<blockquote>
<p>All of this goes into <code>src/routes/index.svelte</code>.</p>
</blockquote>
<p>Next let's add method that runs when we click the button, as well as <code>registering</code> variable indicating loading status:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
  <span class="hljs-keyword">let</span> registering = <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onRegister</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (registering) { <span class="hljs-keyword">return</span>; }
    registering = <span class="hljs-literal">true</span>;

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> AppwriteService.createAccount();
      <span class="hljs-keyword">await</span> goto(<span class="hljs-string">'/casino'</span>);
    } <span class="hljs-keyword">catch</span> (err: any) {
      alert(err.message ? err.message : err);
    } <span class="hljs-keyword">finally</span> {
      registering = <span class="hljs-literal">false</span>;
    }
  }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Finally, let's add a logic to redirect user to <code>/casino</code> route if we can see he is already logged in. This will allow visitors getting to <code>/</code> to be automatically redirected to casino if they are already logged in:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">context</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
  <span class="hljs-keyword">import</span> { browser } <span class="hljs-keyword">from</span> <span class="hljs-string">'$app/env'</span>;
  <span class="hljs-keyword">import</span> { goto } <span class="hljs-keyword">from</span> <span class="hljs-string">'$app/navigation'</span>;
  <span class="hljs-keyword">import</span> { Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/alert'</span>;
  <span class="hljs-keyword">import</span> { AppwriteService, type Profile } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/appwrite'</span>;
  <span class="hljs-keyword">import</span> Loading <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/Loading.svelte'</span>;

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">load</span>(<span class="hljs-params">request: any</span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> account = <span class="hljs-keyword">await</span> AppwriteService.getAccount();
      <span class="hljs-keyword">const</span> profile = <span class="hljs-keyword">await</span> AppwriteService.getProfile(account.$id);

      <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-number">302</span>, <span class="hljs-attr">redirect</span>: <span class="hljs-string">'/casino'</span> };
    } <span class="hljs-keyword">catch</span> (_err: any) { }

    <span class="hljs-keyword">return</span> {};
  }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>That concludes our authentication page 😅 If you are feeling advantageous, feel free to add some cool styles around it.</p>
<h2 id="heading-game-page">🎲 Game Page</h2>
<p>Let's make a file <code>src/routes/casino.svelte</code> to register our new route. In this file, let's start by writing a logic of loading the data. In there let's load user's account information, as well as profile:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">context</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
  <span class="hljs-keyword">import</span> { browser } <span class="hljs-keyword">from</span> <span class="hljs-string">'$app/env'</span>;
  <span class="hljs-keyword">import</span> { goto } <span class="hljs-keyword">from</span> <span class="hljs-string">'$app/navigation'</span>;
  <span class="hljs-keyword">import</span> { Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/alert'</span>;
  <span class="hljs-keyword">import</span> { AppwriteService, type Profile } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/appwrite'</span>;
  <span class="hljs-keyword">import</span> Loading <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/Loading.svelte'</span>;

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">load</span>(<span class="hljs-params">request: any</span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> account = <span class="hljs-keyword">await</span> AppwriteService.getAccount();
      <span class="hljs-keyword">const</span> profile = <span class="hljs-keyword">await</span> AppwriteService.getProfile(account.$id);

      <span class="hljs-keyword">return</span> { <span class="hljs-attr">props</span>: { account, profile } };
    } <span class="hljs-keyword">catch</span> (err: any) {
      <span class="hljs-built_in">console</span>.error(err);

      <span class="hljs-keyword">if</span> (browser) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-number">302</span>, <span class="hljs-attr">redirect</span>: <span class="hljs-string">'/'</span> };
      }
    }

    <span class="hljs-keyword">return</span> {};
  }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<blockquote>
<p>By getting a profile, it is automatically created if not present already. That will automatically give us a balance of 500 almost dollars.</p>
</blockquote>
<p>These information can now be received in a JavaScript variable:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">import</span> type { Models, RealtimeResponseEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'appwrite'</span>;

    <span class="hljs-comment">// Data from module</span>
    <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> account: Models.User&lt;any&gt; | <span class="hljs-literal">undefined</span>;
    <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> profile: Profile | <span class="hljs-literal">undefined</span>;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<blockquote>
<p>You can ignore types import for now. They will come into play later. Just make sure to keep it there 😛</p>
</blockquote>
<p>Next, let's show our account information. For sick of simplicity, we will only show the ID of the account:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-8 text-lg text-brand-700"</span>&gt;</span>
  There we go, account created! Don't believe? This is your ID:
  <span class="hljs-tag">&lt;<span class="hljs-name">b</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold"</span>&gt;</span>{account?.$id}<span class="hljs-tag">&lt;/<span class="hljs-name">b</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>Let's also prepare a button for user to sign out:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">on:click</span>=<span class="hljs-string">{onSignOut}</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center space-x-3 border-brand-600 border-2 hover:border-brand-500 hover:text-brand-500 text-brand-600 rounded-none px-10 py-3"</span>
  &gt;</span>
    {#if signingOut}
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    {/if}
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>Next let's show user's fake dollars balance:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-2xl font-bold text-brand-900 mb-8 mt-8"</span>&gt;</span>Balance<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-3 text-lg text-brand-700"</span>&gt;</span>Your current blalance is:<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-8 text-3xl font-bold text-brand-900"</span>&gt;</span>
  {profile?.balance}
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-brand-900 opacity-25"</span>&gt;</span>Almost Dollars<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>Lastly, let's add section for betting:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">bind:value</span>=<span class="hljs-string">{bet}</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-brand-50 p-4 border-2 border-brand-600 placeholder-brand-600 placeholder-opacity-50 text-brand-600"</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
  <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Enter amount to bet"</span>
/&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">on:click</span>=<span class="hljs-string">{onBet(</span>'<span class="hljs-attr">heads</span>')}
  <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center space-x-3 border-2 border-brand-600 hover:border-brand-500 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"</span>
&gt;</span>
  {#if betting}
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  {/if}
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Heads!<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">on:click</span>=<span class="hljs-string">{onBet(</span>'<span class="hljs-attr">tails</span>')}
  <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center space-x-3 border-2 border-brand-600 hover:border-brand-500 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"</span>
&gt;</span>
  {#if betting}
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  {/if}
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Tails!<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>With all of that, HTML part of our casino page is ready! Let's start adding the logic by first adding method for signing out:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">let</span> signingOut = <span class="hljs-literal">false</span>;
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onSignOut</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (signingOut) { <span class="hljs-keyword">return</span>; }
  signingOut = <span class="hljs-literal">true</span>;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> AppwriteService.signOut();
    <span class="hljs-keyword">await</span> goto(<span class="hljs-string">'/'</span>);
  } <span class="hljs-keyword">catch</span> (err: <span class="hljs-built_in">any</span>) {
    alert(err.message ? err.message : err);
  } <span class="hljs-keyword">finally</span> {
    signingOut = <span class="hljs-literal">true</span>;
  }
}
</code></pre>
<p>Next let's add a logic for our betting section, to allow submitting our coin flip bet:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">let</span> bet: <span class="hljs-built_in">number</span>;
<span class="hljs-keyword">let</span> betting = <span class="hljs-literal">false</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onBet</span>(<span class="hljs-params">side: 'heads' | 'tails'</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!profile || !account || betting) { <span class="hljs-keyword">return</span>; }
    betting = <span class="hljs-literal">true</span>;

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> didWin = <span class="hljs-keyword">await</span> AppwriteService.bet(bet, side);
      <span class="hljs-keyword">if</span> (didWin) {
        alert(<span class="hljs-string">`You won <span class="hljs-subst">${bet}</span> Almost Dollars.`</span>);
      } <span class="hljs-keyword">else</span> {
        alert(<span class="hljs-string">`You lost <span class="hljs-subst">${bet}</span> Almost Dollars.`</span>);
      }
    } <span class="hljs-keyword">catch</span> (err: <span class="hljs-built_in">any</span>) {
      alert(err.message ? err.message : err);
    } <span class="hljs-keyword">finally</span> {
      betting = <span class="hljs-literal">false</span>;
    }
  };
}
</code></pre>
<p>Let's finish off by adding a real-time subscription to our profile. This will automatically update the balance displayed on the website as soon as it changes on the backend:</p>
<pre><code class="lang-ts">$: {
  <span class="hljs-keyword">if</span> (profile &amp;&amp; browser) {
    AppwriteService.subscribeProfile(profile.$id, onProfileChange);
  }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onProfileChange</span>(<span class="hljs-params">response: RealtimeResponseEvent&lt;Profile&gt;</span>) </span>{
  profile = response.payload;
}
</code></pre>
<p>We have just finished the casino page! And.. I think... 🤔</p>
<p>That makes our Almost Casino complete!!! Let's now look at the main topic of this application, 🌟 <strong>SSR</strong> 🌟.</p>
<h2 id="heading-server-side-rendering">⚡ Server Side Rendering  <a></a></h2>
<p>Let's do the magic! 🧙</p>
<p>We start by creating <code>src/hooks.ts</code> file. In there, we intercept each request and get Appwrite authorization headers. We also return it in order to later retrieve it as part of our session object:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cookie <span class="hljs-keyword">from</span> <span class="hljs-string">'cookie'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSession</span>(<span class="hljs-params">event: <span class="hljs-built_in">any</span></span>) </span>{
    <span class="hljs-keyword">const</span> cookieStr = event.request.headers.get(<span class="hljs-string">"cookie"</span>);
    <span class="hljs-keyword">const</span> cookies = cookie.parse(cookieStr || <span class="hljs-string">''</span>);
    <span class="hljs-keyword">const</span> authCookie = cookies[<span class="hljs-string">`a_session_<span class="hljs-subst">${<span class="hljs-keyword">import</span>.meta.env.VITE_APPWRITE_PROJECT_ID.toLowerCase()}</span>_legacy`</span>];

    <span class="hljs-keyword">return</span> {
        authCookie
    }
}
</code></pre>
<blockquote>
<p>For this to work, make sure to install 'cookie' library with <code>npm install cookie</code>.</p>
</blockquote>
<p>Next, at the beginning of every <code>load()</code> function (one in <code>src/routes/index.svelte</code>, one in <code>src/routes/casino.svelte</code>), let's add these two lines to extract our authentication headers from the session, and apply then to Appwrite SDK:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// Using SSR auth cookies (from hook.ts)</span>
<span class="hljs-keyword">if</span> (request.session.authCookie) {
  AppwriteService.setSSR(request.session.authCookie);
}
</code></pre>
<p>With this in place, SSR will now work properly! 😇 You might not see it yet due to cookies following same-domain policy, but we will address that in the deployment section.</p>
<h2 id="heading-deployment">🚀 Deployment  <a></a></h2>
<p>Since we will deploy to <a target="_blank" href="https://vercel.com/">Vercel</a>, let's make sure we use the correct adapter. We start by installing the adapter:</p>
<pre><code class="lang-bash">npm i @sveltejs/adapter-vercel
</code></pre>
<p>Next, let's update our <code>svelte.config.js</code> to use our new adapter:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> adapter <span class="hljs-keyword">from</span> <span class="hljs-string">'@sveltejs/adapter-vercel'</span>; <span class="hljs-comment">// This was 'adapter-auto' previously, we just need to rename to 'adapter-vercel`</span>

<span class="hljs-comment">// ...</span>

<span class="hljs-keyword">const</span> config = {
  <span class="hljs-comment">// ...</span>
  <span class="hljs-attr">kit</span>: {
    <span class="hljs-attr">adapter</span>: adapter({
      <span class="hljs-attr">edge</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">external</span>: [],
      <span class="hljs-attr">split</span>: <span class="hljs-literal">false</span>
    })
  }
};

<span class="hljs-comment">// ...</span>
</code></pre>
<p>This makes our app ready for Vercel! 😎 Let's make a Git repository out of our project, sign in to Vercel and deploy the app. There is no need for any special configuration.</p>
<p>Once the application is deployed, make sure to put both Appwrite and Vercel applications on the same domain in order for SSR to work with authentication cookies.</p>
<p>If your application runs on the root of the domain, for instance, <code>mycasino.com</code>, then your Appwrite could be on any subdomain like <code>api.mycasino.com</code>.</p>
<p>If you plan to host your application on a subdomain, make sure to host it under the subdomain on which Appwrite runs. For instance, you could have Appwrite on domain <code>mycasino.myorg.com</code> and Vercel application on domain <code>app.mycasino.myorg.com</code>.</p>
<h2 id="heading-conclusion">👨‍🎓 Conclusion</h2>
<p>We successfully built a project that confirms Appwrite plays well with all technologies out there. I am really happy I got this article out as there were many requests from the Appwrite community regarding SSR, and now I have a place to point them to, even with code examples 😇</p>
<p>This demo application can also serve as an example for building the same SSR logic with other frameworks such as Angular, Vue, or React. They all follow a really similar structure regarding SSR, and it all comes down to extracting cookies from requests and setting them on Appwrite SDK.</p>
<h2 id="heading-useful-links">🔗 Useful Links</h2>
<p>If you are bookmarking this article, here are some highly valuable links for you to keep an eye on:</p>
<ul>
<li><a target="_blank" href="https://github.com/Meldiron/almost-casino">Almost Casino: GitHub repository</a></li>
<li><a target="_blank" href="https://app.almost-casino.matejbaco.eu/">Almost Casino: Live Demo</a></li>
<li><a target="_blank" href="https://appwrite.io/">Appwrite: Homepage</a></li>
<li><a target="_blank" href="https://appwrite.io/discord">Appwrite: Discord server (for any support)</a></li>
<li><a target="_blank" href="https://kit.svelte.dev/">Svelte Kit: Docs</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Scale Appwrite Storage with DigitalOcean Spaces]]></title><description><![CDATA[Appwrite is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, ...]]></description><link>https://blog.matejbaco.eu/scale-appwrite-storage-with-digitalocean-spaces</link><guid isPermaLink="true">https://blog.matejbaco.eu/scale-appwrite-storage-with-digitalocean-spaces</guid><category><![CDATA[DigitalOcean]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Thu, 24 Mar 2022 12:50:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126008698/FzHPsrGs8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://appwrite.io/">Appwrite</a> is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, realtime databases, cloud functions, webhooks, and much more! If anything is missing, you can easily extend Appwrite using your favorite backend language.</p>
<p>One of the core functionalities of Appwrite is <a target="_blank" href="https://appwrite.io/docs/storage">Appwrite Storage</a>. It allows you to upload, view, download, and query your project files. Appwrite Storage not only takes care of encryption, compression and antivirus scans, it’s also built on top of Appwrite’s flexible yet simple permission system. Appwrite lets you store any files such as text documents, icons, images, videos and more.</p>
<h2 id="heading-what-is-new-in-appwrite-013">🚀 What is new in Appwrite 0.13</h2>
<p>The Appwrite 0.13 release offers a much more powerful Storage service! You can now group your files into <strong>Storage Buckets</strong> for better organization, as well as better control over allowed size and extension. On each bucket, you can also specify a different set of permissions, and toggle additional features such as encryption and compression.</p>
<p>Additionally, you can now upload files with no size limitation. In this release, <strong>chunked upload</strong> is supported and you can split your file into 5MB chunks to gradually upload a file as large as your bucket settings allow. Don’t worry, chunk upload logic is part of our SDKs and if it sees a file larger than 5MB, chunk upload will automatically kick in. One more secret! Our SDKs now accept an <code>onProgress</code> callback that will be triggered each time a new chunk is successfully processed. Implementing progress bars have never been easier 💪</p>
<p>Finally, Appwrite Storage is no longer limited by the size of your hard drive! With Appwrite 0.13, you can now connect the Storage service with cloud storage providers such as <a target="_blank" href="https://www.digitalocean.com/products/spaces">DigitalOcean Spaces</a> or <a target="_blank" href="https://aws.amazon.com/s3/">Amazon S3</a>. With the introduction of these adapters, you no longer need to worry about running out of space, or hitting bandwidth limits due to Appwrite Storage. More providers are coming soon, so stay tuned 😎 In this guide, we’ll take a look at setting up Appwrite Storage using DigitalOcean Spaces as the storage adapter.</p>
<h2 id="heading-digitalocean-spaces-setup">🌊 DigitalOcean Spaces setup</h2>
<p>Before we jump into Appwrite, let’s prepare your DigitalOcean Space. After logging into DigitalOcean:</p>
<ol>
<li>Click the green <code>Create</code> button in the upper right corner and select <code>Spaces</code> from the dropdown. </li>
<li>You will be redirected to the <code>Create Space</code> page, where we need to configure your file storage. Region and CDN configuration are up to you, but in the <code>File listing</code> section, you need to pick <code>Restrict File Listing</code>. This keeps our files secured from the internet, and only communication between servers will be allowed. </li>
<li>Before we finish the setup, provide your space with a unique name. Quick tip, this name will only be stored in the <code>.env</code> file of your Appwrite instance, and users will never see it. That means, the name can be as simple or as complex as you want.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126117487/OS0Qp0Lsx.png" alt="Create a Space" /></p>
<p>After creating the space, you will be presented with a space endpoint. Make sure to note this URL down, you will need it later when connecting Appwrite to DigitalOcean.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126132039/VZcU7NqOZ.png" alt="Space information" /></p>
<p>Last but not least, you need to get a set of access keys to allow Appwrite to read and write from your newly created space. To do that, visit the <code>API</code> page from the navigation on the left, and under the <code>Spaces access keys</code> section we click on <code>Generate New Key</code>. Give the key a name (no worries, this name is only visible to you), and click on the blue tick icon to finish the key creation process. You will be presented with two keys. The one that is on the same line as your key name is called <code>Access key</code>, and the one below is <code>Secret key</code>. Please note both of them down, but be aware, the secret key is a really sensitive piece of information and DigitalOcean won’t show it to you in future. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126151753/2laE0mqxV.png" alt="Space access keys" /></p>
<p>That’s it! You have successfully set up our DigitalOcean Space, and have all the required credentials in your hands. Let’s look at how easy it is to connect Appwrite to our newly created space.</p>
<h2 id="heading-connecting-appwrite-to-digitalocean-spaces">🧰 Connecting Appwrite to DigitalOcean Spaces</h2>
<p>Before you start, make sure that you have the Appwrite instance up and running. Installation of Appwrite is as simple as running one command:</p>
<pre><code>docker run <span class="hljs-operator">-</span>it <span class="hljs-operator">-</span><span class="hljs-operator">-</span>rm \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>volume <span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>run<span class="hljs-operator">/</span>docker.sock:<span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>run<span class="hljs-operator">/</span>docker.sock \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>volume <span class="hljs-string">"$(pwd)"</span><span class="hljs-operator">/</span>appwrite:<span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>code<span class="hljs-operator">/</span>appwrite:rw \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>entrypoint<span class="hljs-operator">=</span><span class="hljs-string">"install"</span> \
    appwrite<span class="hljs-operator">/</span>appwrite:latest
</code></pre><blockquote>
<p>To learn more about the installation process, you can check out our <a target="_blank" href="https://appwrite.io/docs/installation">installation guide</a>.</p>
</blockquote>
<p>Once our Appwrite instance is up and running, we can configure the storage adapter. To do that, let’s enter the <code>.env</code> file and locate <code>_APP_STORAGE_DEVICE</code>. Currently, it’s set to <code>Local</code>, but since we want to use DigitalOcean Spaces, we change it to <code>DOSpaces</code>. With the new storage provider, we are now required to add new variables:</p>
<pre><code><span class="hljs-attr">_APP_STORAGE_DEVICE</span>=DOSpaces
<span class="hljs-attr">_APP_STORAGE_DO_SPACES_BUCKET</span>=YOUT_BUCKET
<span class="hljs-attr">_APP_STORAGE_DO_SPACES_REGION</span>=YOUR_REGION
<span class="hljs-attr">_APP_STORAGE_DO_SPACES_SECRET</span>=YOUR_ACCESS_SECRET
<span class="hljs-attr">_APP_STORAGE_DO_SPACES_ACCESS_KEY</span>=YOUR_ACCESS_KEY
</code></pre><p>Where do you get this information from? 😨 I have a neat trick for you! First of all, you can already fill in access and secret keys, these are the ones you got from earlier steps when creating API key on DigitalOcean. To get the bucket and region, you need to take a look at the space endpoint that we got after creating the DigitalOcean Space. For instance, my endpoint was:</p>
<pre><code><span class="hljs-attribute">https</span>:<span class="hljs-comment">//project-x-bucket.fra1.digitaloceanspaces.com</span>
</code></pre><p>There is a simple trick that is pretty easy to follow to get our bucket name and region. From my URL, the bucket name is <code>project-x-bucket</code>, and the region is <code>fra1</code>. Follow the same logic to extract information from your endpoint.</p>
<p>Once you have all of these environment variables configured, save the file and restart appwrite using <code>docker-compose up -d</code>. Did you notice that Docker is smart enough to only restart containers that use variables you just changed? 🤯</p>
<p>Once the Appwrite instance is restarted, create a new account and a new project. In the left menu, select ‘Storage’ and create a new bucket. Finally, upload a file into our bucket.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126167423/gA_6c7Dwb.png" alt="Appwrite file" /></p>
<p>You can see the file was successfully uploaded, and can start using Appwrite just like usual! To confirm you are actually talking to DigitalOcean Spaces, visit your storage and you'll see the file in there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648126177814/fprFJyL1L.png" alt="DO Spaces file" /></p>
<p>Congrats! You successfully connected Appwrite to DigitalOcean Spaces 🥳 If you plan to use files directly from your Space, there are a few things to keep in mind:</p>
<ol>
<li>Every file is compressed using GZIP compression before storing it in your Space. You will need to decompress the file after download if you plan to use it.</li>
<li>By default, encryption is enabled on newly created buckets in Appwrite Storage. This means you won’t be able to view the file without decrypting it first. I recommend you disable encryption if you plan on downloading the files from your Space.</li>
<li>All compression, encryption and antivirus are skipped for files above 20MB. These files are stored in your Space as plain files.</li>
</ol>
<h2 id="heading-conclusion">👨‍🎓 Conclusion</h2>
<p>The worst nightmare for every project is bad scalability of their application. Thanks to the newly released provider system for Appwrite Storage service, you can now connect Appwrite with external storage providers instead of storing the files on your system. This prevents hitting a hard drive and bandwidth limits, as well as lets you use your favorite provider alongside Appwrite. And as you have seen in the tutorial above, you can easily connect Appwrite with DigitalOcean Spaces in just a few steps!</p>
<p>If you have a project to share, need help or simply want to become a part of the Appwrite community, I would love for you to join our official Appwrite <a target="_blank" href="https://appwrite.io/discord">Discord server</a>. I can’t wait to see what you build!</p>
<h2 id="heading-learn-more"><strong>📚</strong> Learn more</h2>
<p>You can use the following resources to learn more and get help:</p>
<ul>
<li>🚀 <a target="_blank" href="https://github.com/appwrite">Appwrite Github</a></li>
<li>📜 <a target="_blank" href="https://appwrite.io/docs">Appwrite Docs</a></li>
<li>💬 <a target="_blank" href="https://appwrite.io/discord">Discord Community</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Appwrite Storage meets limitless S3]]></title><description><![CDATA[Appwrite is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, ...]]></description><link>https://blog.matejbaco.eu/appwrite-storage-meets-limitless-s3</link><guid isPermaLink="true">https://blog.matejbaco.eu/appwrite-storage-meets-limitless-s3</guid><category><![CDATA[AWS]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Wed, 23 Mar 2022 07:22:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648020002938/iR2ikSqwd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://appwrite.io/">Appwrite</a> is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, realtime databases, cloud functions, webhooks, and much more! If anything is missing, you can easily extend Appwrite using your favorite backend language.</p>
<p>One of the core functionalities of Appwrite is <a target="_blank" href="https://appwrite.io/docs/storage">Appwrite Storage</a>. It allows you to upload, view, download, and query your project files. Appwrite Storage not only takes care of encryption, compression and antivirus scans, it’s also built on top of Appwrite’s flexible, yet simple permission system. Appwrite lets you store any files such as text documents, icons, images, videos and more.</p>
<h2 id="heading-what-is-new-in-appwrite-013">🚀 What is new in Appwrite 0.13</h2>
<p>The Appwrite 0.13 release offers a much more powerful Storage service! You can now group your files into <strong>Storage Buckets</strong> for better organization, as well as better control over allowed size and extension. On each bucket, you can also specify a different set of permissions, and toggle additional features such as encryption and compression.</p>
<p>Additionally, you can now upload files with no size limitation. In this release, <strong>chunked upload</strong> is supported and you can split your file into 5MB chunks to gradually upload a file as large as your bucket settings allow. Don’t worry, chunk upload logic is part of our SDKs and if it sees a file larger than 5MB, chunk upload will automatically kick in. One more secret! Our SDKs now accept an <code>onProgress</code> callback that will be triggered each time a new chunk is successfully processed. Implementing progress bars have never been easier 💪</p>
<p>Finally, Appwrite Storage is no longer limited by the size of your hard drive! With Appwrite 0.13, you can now connect the Storage service with cloud storage providers such as <a target="_blank" href="https://aws.amazon.com/s3/">Amazon S3</a> or <a target="_blank" href="https://www.digitalocean.com/products/spaces">DigitalOcean Spaces</a>. With the introduction of these adapters, you no longer need to worry about running out of space, or hitting bandwidth limits due to Appwrite Storage. More providers are coming soon, so stay tuned 😎In this guide, we’ll take a look at setting up Appwrite Storage using AWS S3 as the storage adapter.</p>
<h2 id="heading-amazon-s3-setup">🌊 Amazon S3 setup</h2>
<p>Before we jump into Appwrite, let’s prepare your Amazon S3 bucket. After logging into the AWS Console:</p>
<ol>
<li>Use the search bar to find the <code>S3</code> service with a bucket icon on a green background and click the orange button saying <code>Create bucket</code>, you will be redirected to the <code>Create bucket</code> page. </li>
<li>Next, configure the Appwrite file storage. Most of the options are up to you, but make sure you keep the <code>Block all public access</code> toggle enabled - you do want to block all public access. <em>(NOTE: I would not recommend enabling server-side encryption, as this feature is supported by Appwrite and you can control it from there.) </em></li>
<li>Finally, make sure to name your bucket with a unique name. Quick tip, this name will only be stored in the <code>.env</code> file of your Appwrite instance, and users will never see it. That means, the name can be as simple or as complex as you want.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648019885091/Ojzj0U9y6.png" alt="S3 Setup" /></p>
<p>Make sure to note down the <code>Bucket name</code> and <code>AWS Region</code>, we will need them later when connecting Appwrite to Amazon S3. In my case, they were <code>appwrite-project-x-bucket</code> and <code>eu-central-1</code>, as seen in the screenshot above.</p>
<p>Last but not least, you need to get a set of access keys to allow Appwrite to read and write from our newly created bucket. To do that, click on the username in the upper right corner, and then select <code>Security credentials</code>. Open the <code>Access keys</code> section and click on a blue button <code>Create New Access Key</code>. In the modal, click on <code>Show Access Key</code>, and you will be presented with two keys. The first one is called <code>Access Key</code>, and the one below is <code>Secret Key</code>. Please note both of them down, but be aware, the secret key is a really sensitive piece of information and AWS won’t show it to you in future.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648019871187/qDY4IrA6L.png" alt="Create access key" /></p>
<p>That’s it! We have successfully set up our Amazon S3, and we have all the required credentials in our hands. Let’s look at how easy it is to connect Appwrite to your newly created bucket.</p>
<h2 id="heading-connecting-appwrite-to-amazon-s3">🧰 Connecting Appwrite to Amazon S3</h2>
<p>Before you start, make sure you have the Appwrite instance up and running. Installation of Appwrite is as simple as running one command:</p>
<pre><code>docker run <span class="hljs-operator">-</span>it <span class="hljs-operator">-</span><span class="hljs-operator">-</span>rm \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>volume <span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>run<span class="hljs-operator">/</span>docker.sock:<span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>run<span class="hljs-operator">/</span>docker.sock \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>volume <span class="hljs-string">"$(pwd)"</span><span class="hljs-operator">/</span>appwrite:<span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>code<span class="hljs-operator">/</span>appwrite:rw \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>entrypoint<span class="hljs-operator">=</span><span class="hljs-string">"install"</span> \
    appwrite<span class="hljs-operator">/</span>appwrite:latest
</code></pre><blockquote>
<p>To learn more about the installation process, you can check out our <a target="_blank" href="https://appwrite.io/docs/installation">installation guide</a>.</p>
</blockquote>
<p>Once our Appwrite instance is up and running, we can configure the storage adapter. To do that, let’s enter the <code>.env</code> file and locate <code>_APP_STORAGE_DEVICE</code>. Currently, it’s set to <code>Local</code>, but since we want to use Amazon S3, we change it to <code>S3</code>. With the new storage provider, we are now required to add new variables:</p>
<pre><code><span class="hljs-attr">_APP_STORAGE_DEVICE</span>=S3
<span class="hljs-attr">_APP_STORAGE_S3_BUCKET</span>=YOUT_BUCKET
<span class="hljs-attr">_APP_STORAGE_S3_REGION</span>=YOUR_REGION
<span class="hljs-attr">_APP_STORAGE_S3_SECRET</span>=YOUR_ACCESS_SECRET
<span class="hljs-attr">_APP_STORAGE_S3_ACCESS_KEY</span>=YOUR_ACCESS_KEY
</code></pre><p>Where do we get this information from? 😨 First of all, you can already fill in access and secret keys, these are the ones we got from earlier steps when creating security credentials in Amazon AWS Console. You should also have a bucket name and region from the bucket creation process.</p>
<p>Once you have all of these environment variables configured, we can save the file and restart appwrite using <code>docker-compose up -d</code>. Did you notice that Docker is smart enough to only restart containers that use variables you just changed? 🤯</p>
<p>Once the Appwrite instance is restarted, create a new account and a new project. In the left menu, select ‘Storage’ and create a new bucket. Finally, upload a file into your bucket.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648019910278/dIB9P-JH2.png" alt="Appwrite file" /></p>
<p>You can see the file was successfully uploaded, and can start using Appwrite just like usual! To confirm you are actually talking to Amazon S3, visit your storage and you'll see the file in there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648019934026/5qTvZWBgd.png" alt="S3 file" /></p>
<p>Congrats! You have successfully connected Appwrite to Amazon S3 🥳 If you plan to use files directly from your amazon bucket, there are a few things to keep in mind:</p>
<ol>
<li>Every file is compressed using GZIP compression before storing it in Amazon S3. You will need to decompress the file after download if you plan to use it.</li>
<li>By default, encryption is enabled on newly created buckets in Appwrite Storage. This means you won’t be able to view the file without decrypting it first. I recommend you disable encryption if you plan on downloading the files from your Amazon S3.</li>
<li>All compression, encryption and antivirus are skipped for files above 20MB. These files are stored in your Amazon S3 as plain files.</li>
</ol>
<h2 id="heading-conclusion">👨‍🎓 Conclusion</h2>
<p>The worst nightmare for every project is bad scalability of their application. Thanks to the newly released provider system for Appwrite Storage service, you can now connect Appwrite with external storage providers instead of storing the files on your system. This prevents hitting a hard drive and bandwidth limits, as well as lets you use your favorite provider alongside Appwrite. And as you have seen in the tutorial above, you can easily connect Appwrite with Amazon S3 in just a few steps!</p>
<p>If you have a project to share, need help or simply want to become a part of the Appwrite community, I would love for you to join our official Appwrite <a target="_blank" href="https://appwrite.io/discord">Discord server</a>. I can’t wait to see what you build!</p>
<h2 id="heading-learn-more">📚 Learn more</h2>
<p>You can use the following resources to learn more and get help:</p>
<ul>
<li>🚀 <a target="_blank" href="https://github.com/appwrite">Appwrite Github</a></li>
<li>📜 <a target="_blank" href="https://appwrite.io/docs">Appwrite Docs</a></li>
<li>💬 <a target="_blank" href="https://appwrite.io/discord">Discord Community</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Start Selling Online Using Appwrite and Stripe]]></title><description><![CDATA[Appwrite is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, ...]]></description><link>https://blog.matejbaco.eu/start-selling-online-using-appwrite-and-stripe</link><guid isPermaLink="true">https://blog.matejbaco.eu/start-selling-online-using-appwrite-and-stripe</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Tue, 22 Mar 2022 13:27:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955189115/kw_QtmQ-g.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://appwrite.io/">Appwrite</a> is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, realtime databases, cloud functions, webhooks, and much more! If anything is missing, you can easily extend Appwrite using your favorite backend language.</p>
<p>Every website is a unique project with unique requirements. Appwrite understands the need for backend customization and provides <a target="_blank" href="https://appwrite.io/docs/functions">Appwrite Functions</a> to allow you to do exactly that. Thanks to 7 supported programming languages and more coming soon, Appwrite lets you use the language you are the most familiar with for your backend needs.</p>
<h2 id="heading-what-is-new-in-appwrite-013">🚀 What Is New in Appwrite 0.13</h2>
<p>Appwrite 0.13 introduced new features regarding storage, CLI, and functions. With a fully rewritten execution model, Appwrite now allows you to run functions as quick as 1 millisecond! 🤯 With such a performance, Appwrite now also ships with <strong>synchronous execution</strong>, allowing you to execute a function and get a response within one HTTP request.</p>
<p>Thanks to the new execution model, Appwrite 0.13 also allows the creation of global variables to share cache between multiple executions of a function. This drastically improves the performance of real-world applications, as they only need to load dependencies, initiate 3rd party communication, and pre-load resources in the first execution.</p>
<p>Last but not least, Appwrite got a new build process that is capable of downloading your project dependencies on the server side, so you no longer need to deploy your function with dependencies. This makes the flow much simpler, deployments smaller, and developers happier 😍</p>
<h2 id="heading-online-payments-with-stripe">💸 Online Payments with Stripe</h2>
<p><a target="_blank" href="https://stripe.com/">Stripe</a> is a huge set of payments products that allows you to do business online. Stripe allows you to receive payments online, set up and manage subscriptions, automatically include taxes into payments, manage the online receipt, generate a checkout page, and much more. Stripe proudly supports multiple payment methods and countries, which makes it one of the best options for any online product.</p>
<p>Stripe is loved by developers too! Online payments are hard… Stripe managed to create a fast, secure and easy to understand environment that lets developers set up online payments within a few minutes.</p>
<p>From a technical point of view, Stripe payments are initiated using REST API, and payment status updates are sent through webhooks. Let’s look at how you can use Appwrite Functions to securely communicate with Stripe to implement online payments into an application.</p>
<h2 id="heading-what-is-a-webhook">🙋 What Is a Webhook?</h2>
<p>Webhook is an HTTP request sent from one server to another, to inform it about some change. Webhooks are a smarter alternative to pulling data through an API server if you need to quickly adapt to an external change.</p>
<p>Stripe uses webhooks to inform applications about changes to the status of a payment. For a second let’s imagine webhooks were not implemented in Stripe, how would you know a payment was successful? For each ongoing payment, you would need to have a loop to send API requests to get payment status every few seconds and don’t stop until you have a status change. As you can imagine, this would be a resource-consuming solution that wouldn’t scale well with many payments pending at the same time, hitting API limits in the worst scenarios. Thanks to webhooks, you can give Stripe an URL, and the Stripe server will hit the URL with an HTTP request, providing a bunch of data about what has changed.</p>
<p>Similarly to Stripe, Appwrite also supports webhooks and can trigger HTTP requests when a change occurs inside Appwrite, such as a new user registered, or a database change. That means Appwrite can send out webhooks, but can it receive one? 🤔</p>
<h2 id="heading-appwrite-webhook-proxy">🪝 Appwrite Webhook Proxy</h2>
<p>Appwrite can receive webhook requests by default, thanks to Appwrite Functions. There is an endpoint in Appwirte HTTP API that can create a function execution. This method allows passing data in, and also providing request headers. That’s all you need for such a webhook listener, but there is one small hiccup.</p>
<p>Looking at <a target="_blank" href="https://appwrite.io/docs/server/functions?sdk=nodejs-default#functionsCreateExecution">Appwrite documentation</a>, it expects a JSON body where all data is stringified under the <code>data</code> key. On the other hand, looking at <a target="_blank" href="https://stripe.com/docs/webhooks#check-event-objects">Stripe documentation</a>, it sends a webhook with all data in the root level as a JSON object.</p>
<p>Alongside this schema miss-match, Appwrite also expects some custom headers (such as API key), which Stripe cannot send. This problem can be solved by a simple proxy server that can properly map between these two schemas, and apply authentication headers.</p>
<p>You can expect official implementation from Appwrite itself, but as of right now, you can use <a target="_blank" href="https://github.com/Meldiron/appwrite-webhook-proxy">Meldiron’s Appwrite webhook proxy</a>. This project adds a configuration into your Appwrite setup that defined a new <code>/v1/webhook-proxy</code> endpoint in Appwrite API to solve the problem from earlier. Later in the article, we will take a look at how to set up this webhook proxy, and how to connect it to Stripe.</p>
<h2 id="heading-lets-code-a-store">🛒 Let’s Code a Store</h2>
<p>To present Stripe integration in Appwrite, I decided to create a simple application <strong>cookie store</strong>, where a customer can buy one of two cookie packs. After payment, users can look at their order history and see a payment status. This is a minimal implementation that does not include invoicing, fulfillment, or any eCommerce logic. The project was made with simplicity in mind to serve as a learning resource for anyone integrating Stripe into their Appwrite projects.</p>
<p>The application was made using <a target="_blank" href="https://nuxtjs.org/">NuxtJS</a> framework with <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a>, and <a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a> for designing utility classes. You can follow along, or download the source code from the <a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store">GitHub repository</a>.</p>
<h3 id="heading-stripe-setup">Stripe Setup</h3>
<p>Let’s start by properly setting up our Stripe account, to make sure we have all secrets we might need in the future. For this example, we will be using test mode, but the same steps could be followed in production mode.</p>
<p>You start by visiting the <a target="_blank" href="https://stripe.com/">Stripe</a> website and signing up. Once in the dashboard, you can switch to the <code>Developers</code> page and enter the <code>API keys</code> tab. In there, you click the <code>Reval key</code> button and copy this key. It will be used later when setting up <code>createPayment</code> function in Appwrite.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955279119/8baq_bl8g.png" alt="Stripe API key" /></p>
<p>Next, let’s switch to the <code>Webhooks</code> tab, and set up a new endpoint. When adding an endpoint, make sure to use URL <code>http://YOR_ENDPOINT/v1/webhook-proxy</code>, and provide any description you want. Last but not least, you select events to listen to, in the case of simple online payment, you only need events <code>payment_intent.succeeded</code> and <code>payment_intent.canceled</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955357407/kO1z8TEIQ.png" alt="Stripe webhook setup" /></p>
<p>After adding the endpoint, copy your <code>Signing secret</code>, as you will need this in <code>updatePayment</code> Appwrite Function later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955306275/f93h2U4Q-.png" alt="Stripe webhook secret" /></p>
<h3 id="heading-appwrite-project-setup">Appwrite Project Setup</h3>
<p>Before diving into frontend development, you first set up the Appwrite project. After following <a target="_blank" href="https://appwrite.io/docs/installation">installation instructions</a> and signing up, you can create a project with a custom project ID <code>cookieShop</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955397749/yKY1JB8Kd.png" alt="new Appwrite project" /></p>
<p>Once the project is created, let’s hop into the <code>Services</code> tab on the <code>Settings</code> page. Here you can easily disable services that you won’t be using in our project. In your application, you will only be using account, database and function services. Make sure to keep this enabled, and disable the rest.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955426195/eqpow7gz8.png" alt="Appwrite project service settings" /></p>
<p>Last but not least, let’s open the <code>Settings</code> tab on the <code>Users</code> page. Here you can disable all authentication methods except anonymous session, as this will be the only one your application will use.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647955439412/TnzkjhjsZ.png" alt="Appwrite users settings" /></p>
<p>With all of these configurations in place, your Appwrite project is ready! 🎉</p>
<p>Now, you need to apply programmatic setup from the cookie store <a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store">GitHub repository</a> that sets up database structure and prepares Appwrite Functions. After cloning the repository and setting up <a target="_blank" href="https://appwrite.io/docs/command-line">Appwrite CLI</a>, all you need to do is to run <code>appwrite deploy –all</code> to apply all of the programmatic setups. If you are interested in understanding the underlying code of these Appwrite Functions, you can check them out in respective folders:</p>
<ul>
<li><a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store/tree/master/functions/createPayment">createPayment</a> (NodeJS)</li>
<li><a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store/tree/master/functions/updatePayment">updatePayment</a> (NodeJS)</li>
</ul>
<p>Once these functions are deployed, you need to set their environment variables. You visit <code>Functions</code> in your Appwrite Console and open up the <code>Settings</code> tab of your <code>createPayment</code> function. In there, near the end of the settings, you need to add a variable called <code>STRIPE_KEY</code> with your secret key from the Stripe dashboard. Next, you switch to settings of <code>updatePayment</code> and set up a few environments variables there:</p>
<ul>
<li><code>STRIPE_SIGNATURE</code> - Webhook signature key from Stripe dashboard.</li>
<li><code>APPWRITE_FUNCTION_ENDPOINT</code> - Endpoint of your Appwrite instance, found in <code>Settings</code>.</li>
<li><code>APPWRITE_FUNCTION_API_KEY</code> - Appwrite project API key. You can generate one in the left menu.</li>
</ul>
<p>With that configured, let’s see how our Appwrite Functions actually work! 💻</p>
<h3 id="heading-appwrite-functions">Appwrite Functions</h3>
<p>To better understand our Appwrite Functions logic, let’s look at their source code. Both functions are written in Node.JS</p>
<h4 id="heading-1-create-payment">1. Create Payment</h4>
<p>First of all, you add Stripe library to our code, as you will be creating a payment in this function:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> stripe = <span class="hljs-built_in">require</span>(<span class="hljs-string">'stripe'</span>)
</code></pre>
<p>Next, you set up a variable holding all possible packs (products), and their basic information:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> packages = [
  {
    <span class="hljs-attr">id</span>: <span class="hljs-string">'pack1'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'Medium Cookie Pack'</span>,
    <span class="hljs-attr">description</span>: <span class="hljs-string">'Package incluces 1 cookie'</span>,
    <span class="hljs-attr">price</span>: <span class="hljs-number">1.99</span>,
    <span class="hljs-attr">preview</span>: <span class="hljs-string">'/pack1.jpg'</span>,
  },
  {
    <span class="hljs-attr">id</span>: <span class="hljs-string">'pack2'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'Large Cookie Pack'</span>,
    <span class="hljs-attr">description</span>: <span class="hljs-string">'Package incluces 6 cookies'</span>,
    <span class="hljs-attr">price</span>: <span class="hljs-number">4.99</span>,
    <span class="hljs-attr">preview</span>: <span class="hljs-string">'/pack2.jpg'</span>,
  },
]
</code></pre>
<p>You continue by setting up a function that will get executed when an execution is created:</p>
<pre><code class="lang-jsx"><span class="hljs-built_in">module</span>.exports = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-comment">// Future code goes in here</span>
}
</code></pre>
<p>Inside your function, let’s make sure function as properly configured in Appwrite, and provides required environment variables:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Setup</span>
  <span class="hljs-keyword">if</span> (!req.env.STRIPE_KEY) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Environment variables are not set.'</span>)
  }
</code></pre>
<p>Next, let’s validate user input - payload:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Prepate data</span>
  <span class="hljs-keyword">const</span> payload = <span class="hljs-built_in">JSON</span>.parse(req.payload)
  <span class="hljs-keyword">const</span> stripeClient = stripe(req.env.STRIPE_KEY)

  <span class="hljs-keyword">const</span> package = packages.find(<span class="hljs-function">(<span class="hljs-params">pack</span>) =&gt;</span> pack.id === payload.packId)

  <span class="hljs-keyword">if</span> (!package) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Could not find the pack.'</span>)
  }
</code></pre>
<p>You continue by creating a Stripe payment session:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Create Stripe payment</span>
  <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> stripeClient.checkout.sessions.create({
    <span class="hljs-attr">line_items</span>: [
      {
        <span class="hljs-attr">price_data</span>: {
          <span class="hljs-attr">currency</span>: <span class="hljs-string">'eur'</span>,
          <span class="hljs-attr">product_data</span>: {
            <span class="hljs-attr">name</span>: package.title,
            <span class="hljs-attr">description</span>: package.description,
          },
          <span class="hljs-attr">unit_amount</span>: package.price * <span class="hljs-number">100</span>,
        },
        <span class="hljs-attr">quantity</span>: <span class="hljs-number">1</span>,
      },
    ],
    <span class="hljs-attr">mode</span>: <span class="hljs-string">'payment'</span>,
    <span class="hljs-attr">success_url</span>: payload.redirectSuccess,
    <span class="hljs-attr">cancel_url</span>: payload.redirectFailed,
    <span class="hljs-attr">payment_intent_data</span>: {
      <span class="hljs-attr">metadata</span>: {
        <span class="hljs-attr">userId</span>: req.env.APPWRITE_FUNCTION_USER_ID,
        <span class="hljs-attr">packageId</span>: package.id,
      },
    },
  })
</code></pre>
<p>Last but not least, let’s return stripe payment session URL, so client can be redirected to the payment:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Return redirect URL</span>
  res.json({
    <span class="hljs-attr">paymentUrl</span>: session.url,
  })
</code></pre>
<h4 id="heading-2-update-payment">2. Update Payment</h4>
<p>Similar to our first function, you require libraries and set up a main function:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> stripe = <span class="hljs-built_in">require</span>(<span class="hljs-string">'stripe'</span>)
<span class="hljs-keyword">const</span> sdk = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node-appwrite'</span>)

<span class="hljs-built_in">module</span>.exports = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-comment">// Future code goes in here</span>
}
</code></pre>
<p>Did you notice you imported Appwrite this time? That’s right! This function is executed by Stripe webhook when a payment session status changes. This means, you will need to update the Appwrite document with a new status, so you need a proper connection with the API.</p>
<p>Anyway, you continue by validating environment variables, but this time you also initialize Appwrite SDK:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Setup Appwrite SDK</span>
  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> sdk.Client()
  <span class="hljs-keyword">const</span> database = <span class="hljs-keyword">new</span> sdk.Database(client)

  <span class="hljs-keyword">if</span> (
    !req.env.APPWRITE_FUNCTION_ENDPOINT ||
    !req.env.APPWRITE_FUNCTION_API_KEY ||
    !req.env.STRIPE_SIGNATURE
  ) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Environment variables are not set.'</span>)
  }

  client
    .setEndpoint(req.env.APPWRITE_FUNCTION_ENDPOINT)
    .setProject(req.env.APPWRITE_FUNCTION_PROJECT_ID)
    .setKey(req.env.APPWRITE_FUNCTION_API_KEY)
</code></pre>
<p>Next, let’s parse the function input (payload), and validate it using Stripe:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Prepate data</span>
  <span class="hljs-keyword">const</span> stripeSignature = req.env.STRIPE_SIGNATURE
  <span class="hljs-keyword">const</span> payload = <span class="hljs-built_in">JSON</span>.parse(req.payload)

  <span class="hljs-comment">// Validate request + authentication check</span>
  <span class="hljs-keyword">let</span> event = stripe.webhooks.constructEvent(
    payload.body,
    payload.headers[<span class="hljs-string">'stripe-signature'</span>],
    stripeSignature
  )
</code></pre>
<p>Furthermore, you can parse data from Stripe event and pick information relevant to your usage:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Prepare results</span>
  <span class="hljs-keyword">const</span> status =
    event.type === <span class="hljs-string">'payment_intent.succeeded'</span>
      ? <span class="hljs-string">'success'</span>
      : event.type === <span class="hljs-string">'payment_intent.canceled'</span>
      ? <span class="hljs-string">'failed'</span>
      : <span class="hljs-string">'unknown'</span>

  <span class="hljs-keyword">const</span> userId = event.data.object.charges.data[<span class="hljs-number">0</span>].metadata.userId
  <span class="hljs-keyword">const</span> packId = event.data.object.charges.data[<span class="hljs-number">0</span>].metadata.packageId
  <span class="hljs-keyword">const</span> paymentId = event.data.object.id

  <span class="hljs-keyword">const</span> <span class="hljs-built_in">document</span> = {
    status,
    userId,
    packId,
    paymentId,
    <span class="hljs-attr">createdAt</span>: <span class="hljs-built_in">Date</span>.now(),
  }
</code></pre>
<p>To finish it off, let’s add a logic to update or create a document, depending on if it already exists or not:</p>
<pre><code class="lang-jsx">  <span class="hljs-comment">// Check if document already exists</span>
  <span class="hljs-keyword">const</span> existingDocuments = <span class="hljs-keyword">await</span> database.listDocuments(
    <span class="hljs-string">'orders'</span>,
    [<span class="hljs-string">`paymentId.equal('<span class="hljs-subst">${paymentId}</span>')`</span>],
    <span class="hljs-number">1</span>
  )

  <span class="hljs-keyword">let</span> outcome

  <span class="hljs-keyword">if</span> (existingDocuments.documents.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// Document already exists, update it</span>
    outcome = <span class="hljs-string">'updateDocument'</span>
    <span class="hljs-keyword">await</span> database.updateDocument(
      <span class="hljs-string">'orders'</span>,
      existingDocuments.documents[<span class="hljs-number">0</span>].$id,
      <span class="hljs-built_in">document</span>,
      [<span class="hljs-string">`user:<span class="hljs-subst">${userId}</span>`</span>],
      []
    )
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Document doesnt exist, create one</span>
    outcome = <span class="hljs-string">'createDocument'</span>
    <span class="hljs-keyword">await</span> database.createDocument(
      <span class="hljs-string">'orders'</span>,
      <span class="hljs-string">'unique()'</span>,
      <span class="hljs-built_in">document</span>,
      [<span class="hljs-string">`user:<span class="hljs-subst">${userId}</span>`</span>],
      []
    )
  }
</code></pre>
<p>Finally, let’s return what you just did as a response, so you can inspect execution response in Appwrite Console when you need to double-check what happened in some specific payment:</p>
<pre><code class="lang-jsx">  res.json({
    outcome,
    <span class="hljs-built_in">document</span>,
  })
</code></pre>
<h3 id="heading-appwrite-webhook-proxy">Appwrite Webhook Proxy</h3>
<p>As mentioned earlier, you will need to use <a target="_blank" href="https://github.com/Meldiron/appwrite-webhook-proxy">Meldiron’s webhook proxy</a> to translate Stripe’s schema to a schema that Appwrite API supports. To do that, you will add a new container into the Appwrite Docker containers stack, which will add a new endpoint to Appwrite API.</p>
<p>Let’s start by adding a new container definition inside the <code>docker-compose.yml</code> file in an <code>appwrite</code> folder:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3"</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">appwrite-webhook-proxy:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">meldiron/appwrite-webhook-proxy:v0.0.4</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">appwrite-webhook-proxy</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.enable=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.constraint-label-stack=appwrite"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.docker.network=appwrite"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.services.appwrite_webhook_proxy.loadbalancer.server.port=4444"</span>
      <span class="hljs-comment"># http</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_http.entrypoints=appwrite_web</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_http.rule=PathPrefix(`/v1/webhook-proxy`)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_http.service=appwrite_webhook_proxy</span>
      <span class="hljs-comment"># https</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_https.entrypoints=appwrite_websecure</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_https.rule=PathPrefix(`/v1/webhook-proxy`)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_https.service=appwrite_webhook_proxy</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_https.tls=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.appwrite_webhook_proxy_https.tls.certresolver=dns</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">appwrite</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">appwrite</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_PROXY_APPWRITE_ENDPOINT</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_PROXY_APPWRITE_PROJECT_ID</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_PROXY_APPWRITE_API_KEY</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_PROXY_APPWRITE_FUNCTION_ID</span>
  <span class="hljs-comment"># ...</span>
<span class="hljs-comment"># ...</span>
</code></pre>
<p>With this in place, a new proxy server will be listening on <code>/v1/webhook-proxy</code> endpoint.</p>
<p>Now let’s update the <code>.env</code> file in the same <code>appwrite</code> folder to add authentication variables this container needs for proper secure communication:</p>
<pre><code><span class="hljs-attr">WEBHOOK_PROXY_APPWRITE_ENDPOINT</span>=https://YOUR_ENDPOINT/v1
<span class="hljs-attr">WEBHOOK_PROXY_APPWRITE_PROJECT_ID</span>=YOUR_PROJECT_ID
<span class="hljs-attr">WEBHOOK_PROXY_APPWRITE_API_KEY</span>=YOUR_API_KEY
<span class="hljs-attr">WEBHOOK_PROXY_APPWRITE_FUNCTION_ID</span>=updatePayment
</code></pre><p>Finally, let’s spin up the container by running <code>docker-compose up -d</code>. With all of that in place, you can now point Stripe to <code>https://YOR_ENDPOINT/v1/webhook-proxy</code> , and Stripe will start executing your <code>updatePayment</code> function while providing all data in a proper schema.</p>
<h3 id="heading-frontend-website-setup">Frontend Website Setup</h3>
<p>A process of designing frontend is not the focus of this article, so if you are interested in details of implementation, make sure to check out the <a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store">GitHub repository</a> of this project.</p>
<p>With that out of the way, let’s look at communication between the frontend and Appwrite project. All of this communication is implemented in a separated <a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store/blob/master/services/appwrite.ts">appwrite.ts</a> file that holds functions for:</p>
<ul>
<li>Authentication</li>
<li>Payment</li>
<li>Order History</li>
</ul>
<p>Before coding functions for these services, let’s set up our service file and do all of the initial setups:</p>
<pre><code class="lang-tsx">import { Appwrite, Models } from "appwrite";

if (!process.env.appwriteEndpoint || !process.env.appwriteProjectId) {
    throw new Error("Appwrite environment variables not properly set!");
}

const sdk = new Appwrite();
sdk
    .setEndpoint(process.env.appwriteEndpoint)
    .setProject(process.env.appwriteProjectId);

const appUrl = process.env.baseUrl;

export type Order = {
    status: string,
    userId: string,
    packId: string,
    paymentId: string,
    createdAt: number
} &amp; Models.Document;
</code></pre>
<p>Let’s start by creating trio of the most important authentication functions. You will need one to login, one to log out, and one to check if visitor is logged in. All of this can be done within a few lines of code when using AppwriteSDK:</p>
<pre><code class="lang-tsx">export const AppwriteService = {
    async logout(): Promise&lt;boolean&gt; {
        try {
            await sdk.account.deleteSession("current");
            return true;
        } catch (err) {
            console.error(err);
            alert("Something went wrong. Please try again later.");
            return false;
        }
    },

    async login(): Promise&lt;void&gt; {
        await sdk.account.createAnonymousSession();
    },

    async getAuthStatus(): Promise&lt;boolean&gt; {
        try {
            await sdk.account.get();
            return true;
        } catch (err) {
            console.error(err);
            return false;
        }
    },

    // Future code goes in here
};
</code></pre>
<p>Next, you create a function that will trigger our previously coded <code>createPayment</code> Appwrite Function, and use <code>url</code> from the response to redirect user to Stripe, where they can pay they order:</p>
<pre><code class="lang-tsx">    async buyPack(packId: string): Promise&lt;boolean&gt; {
        try {
            const executionResponse: any = await sdk.functions.createExecution("createPayment", JSON.stringify({
                redirectSuccess: `${appUrl}/cart-success`,
                redirectFailed: `${appUrl}/cart-error`,
                packId
            }), false);

            if (executionResponse.status === 'completed') {
            } else {
                throw new Error(executionResponse.stdout + "," + executionResponse.err);
            }

            const url = JSON.parse(executionResponse.stdout).paymentUrl;
            window.location.replace(url);

            return true;
        } catch (err) {
            console.error(err);
            alert("Something went wrong. Please try again later.");
            return false;
        }
    },
</code></pre>
<p>Last but not least, let's implement a method to get user’s order history that supports offset pagination:</p>
<pre><code class="lang-tsx">    async getOrders(page = 1): Promise&lt;Models.DocumentList&lt;Order&gt; | null&gt; {
        try {
            const offset = (page - 1) * 10;
            const ordersResponse = await sdk.database.listDocuments&lt;Order&gt;("orders", undefined, 10, offset, undefined, undefined, ['createdAt'], ['DESC']);

            return ordersResponse;
        } catch (err) {
            console.error(err);
            alert("Something went wrong. Please try again later.");
            return null;
        }
    }
</code></pre>
<p>With all of this login in place, all you need to do is to finish off the rest of the frontend application by creating pages, components, and hooking into our <code>AppwriteService</code> to talk to the Appwrite backend.</p>
<p>You have just successfully created your very own store using Appwrite and Stripe! 👏 If there are any concerns about skipped parts of the frontend code, and I can’t stress this enough, make sure to check out the whole <a target="_blank" href="https://github.com/Meldiron/appwrite-cookie-store">GitHub repository</a> of this project, that holds a fully working demo application. There are some screenshots too! 👀</p>
<h2 id="heading-conclusion">👨‍🎓 Conclusion</h2>
<p>The ability to integrate your application with 3rd party tools and APIs can become critical for any scalable application. Thanks to Appwrite 0.13, as you just experienced, Appwrite Functions can now communicate both ways, allowing you to prepare your projects without any limitations. This not only means you can implement pretty much any payment gateway into your Appwrite project, but it also means all of you can enjoy preparing your Appwrite-based applications without any limitations!</p>
<p>If you have a project to share, need help or simply want to become a part of the Appwrite community, I would love for you to join our official Appwrite <a target="_blank" href="https://appwrite.io/discord">Discord server</a>. I can’t wait to see what you build!</p>
<h2 id="heading-learn-more">📚 Learn More</h2>
<p>You can use the following resources to learn more and get help:</p>
<ul>
<li>🚀 <a target="_blank" href="https://github.com/appwrite">Appwrite Github</a></li>
<li>📜 <a target="_blank" href="https://appwrite.io/docs">Appwrite Docs</a></li>
<li>💬 <a target="_blank" href="https://appwrite.io/discord">Discord Community</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[8 reasons to fall in ❤️ with Appwrite]]></title><description><![CDATA[I prepared a small quiz for you at the end of the article, so keep reading! 👀 Do you know all of the answers? 😈

Since I became active in Appwrite's Discord server, I noticed so many people getting excited about Appwrite that I decided to dedicate ...]]></description><link>https://blog.matejbaco.eu/8-reasons-to-fall-in-with-appwrite</link><guid isPermaLink="true">https://blog.matejbaco.eu/8-reasons-to-fall-in-with-appwrite</guid><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Sun, 25 Jul 2021 14:03:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300650687/xSaDPuV8C.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>I prepared a small quiz for you at the end of the article, so keep reading! 👀 Do you know all of the answers? 😈</p>
</blockquote>
<p>Since I became active in <a target="_blank" href="https://appwrite.io/discord">Appwrite's Discord server</a>, I noticed <strong>so many</strong> people getting excited about Appwrite that I decided to dedicate one article to all features people ❤️ about Appwrite.</p>
<p>Sure, I could write about everything that Appwrite can do, but that is not the focus of this article. If you are interested in that, check out <a target="_blank" href="https://dev.to/appwrite">Appwrite's articles</a>. This article will show you <strong>practical reasons</strong> to use Appwrite for your next side project!</p>
<h2 id="1-free-now-free-forever">1. 💸 Free now, free forever!</h2>
<p>Thanks to Appwrite being open-sourced, we can expect all future updates to be free for everyone. This doesn't seem like a huge deal, but companies such as Google (Firebase) or Amazon (AWS) have their pricing set, so it looks promising at first but gets <strong>really</strong> expensive once your business grows. With Appwrite, you can choose your own server provider and scale without spending half of your budget.
I can happily confirm that Appwrite will stay open-sourced and keep all updates for free even after it gets released (1.0.0). Appwrite plans to provide cloud hosting and other juicy features for companies, but they don't intend to have any cloud-exclusive features!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300631889/7y2xZO2Rm.gif" alt="GIF about money" /></p>
<h2 id="2-wonderful-community">2. 🤩 Wonderful community</h2>
<p>I have joined many developer communities to learn about technology... Heck, I even hit the 100 server limit on Discord that made me upgrade to Discord nitro!</p>
<p>Without the slightest hesitation, I can tell you that Appwrite has the most friendly, active and helpful Discord community I have ever seen. It doesn't matter if you are a student learning to build your first project or a senior with ten years of experience; there is always someone to answer your questions on your level. Alongside the Appwrite core team and moderators, there are always at least 200 online members on the server ready to chat with you about anything. Yep, anything! A few days ago, we talked about <a target="_blank" href="https://isevenapi.xyz/">API</a> to check if a number is even 🧠</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300633758/XWiY6-RcxW.gif" alt="GIF about community" /></p>
<h2 id="3-1-production-100-projects">3. 🔥 1 production, 100 projects</h2>
<p>Most open-source platforms are not built to handle multiple projects, and some are even unable to scale! That becomes a nightmare once your business grows, and you may be forced to switch to something else...</p>
<p>Great news for you! Appwrite can do both. You can host multiple projects on one Appwrite instance <strong>AND</strong> you can set up a Docker swarm to scale it across multiple servers.</p>
<p>{% post https://dev.to/appwrite/30daysofappwrite-docker-swarm-integration-2io9 %}</p>
<p>I personally find this <strong>really</strong> useful because only the Appwrite itself uses hardware resources, not the project. You can prepare a website for small local businesses and host it pretty much for free since your server hardware will barely notice their tiny traffic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300636159/aU0f2h54b.gif" alt="GIF about production" /></p>
This is not a Firebase reference

<h2 id="4-usage-statistics">4. 📈 Usage statistics</h2>
<p>This is pretty related to the previous point...</p>
<p>Let's say you used Appwrite on 30 projects for your clients. Do you know how much to invoice the client for when talking about server expenses? Thanks to usage statistics, you can easily see which projects take the most space, have the most requests, or use the most CPU time for function executions. You can also see how many errors happened to functions in the past, so we could say you have a tiny alerting system in your hands. Last but not least, you can see bandwidth usage on each project separately and debug most of "it's slow" problems quickly.</p>
<blockquote>
<p>Oh man, at this point, I was already impressed by what Appwrite is capable of, and it is only the middle of this article! 🔥</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300638313/-a0TX2xD4.gif" alt="GIF about statistics" /></p>
<h2 id="5-api-for-everything">5. 🧰 API for everything</h2>
<p>Appwrite tries to provide as many APIs as possible, so you can build the application without having to bundle 10 API servers together. Appwrite can help you to get:</p>
<ul>
<li>List of countries and their flags, continents, currencies, languages, phone codes, browser icons, credit card icons...</li>
<li>Image preview (resize, rotate, ...)</li>
<li>Placeholder photo of a user (their initials)</li>
<li>QR code from a text</li>
<li>Favicon from website URL</li>
</ul>
<p>Can you see it? 😮 With Appwrite, you can focus on preparing the application instead of searching for <em>the best API</em> to do tiny tasks for your applications.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300640562/6IFyOgVeJ.gif" alt="GIF about APIs" /></p>
<h2 id="6-task-manager">6. 📃 Task manager</h2>
<p>We have all been there... We have... We have all created CRONS on a Linux server, tested it for half of the week to make sure it works properly and then it stopped working anyway because the server got restarted...</p>
<p>I personally have been looking for a good cron alternative and <strong>fell in love</strong> with Appwrite's task manager. Actually, if someone would use Appwrite only to host functions and run tasks, I would not be surprised.</p>
<p>Using Appwrite, you can schedule an automatic function execution using cron syntax. This function can do anything from sending newsletter emails to buying you a pizza. No limitations <strong>at all</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300642566/mV2_kc7FR.gif" alt="GIF about crons" /></p>
<h2 id="7-continuous-improvement">7. 💪 Continuous improvement</h2>
<p>Appwrite is still in active development, so we can expect awesome new features with every update! From the Discord community, I found about these features:</p>
<ul>
<li><strong>Database refactor</strong> to speed up the database and introduce new possibilities</li>
<li><strong>Realtime</strong> to easily keep frontend and backend in sync</li>
<li><strong>Synchronous functions</strong> that makes using Appwrite functions as a custom API server possible</li>
<li><strong>Static hosting</strong> to deploy your web application directly into Appwrite</li>
<li><strong>Webhooks improvement</strong> so we can not only send webhook requests but also receive them</li>
<li><strong>Storage buckets</strong> to increase the security of file storage</li>
<li><strong>Locale API improvements</strong> so you can translate your whole application using Appwrite</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300644652/h6VC7S30X.gif" alt="GIF about improvement" /></p>
<h2 id="8-over-10-sdks">8. 🦄 Over 10 SDKs</h2>
<p>Appwrite has done an <strong>amazing</strong> job getting as close to developers as possible. No matter what coding language you use, you can easily start using Appwrite thanks to their client and server-side SDK libraries. <strong>Instead of the developer adapting to a platform, the platform adapted to the developer.</strong></p>
<p>As far as I am concerned, Appwrite is actively improving Flutter SDK and is working on Unity SDK to deliver the platform to even more developers.</p>
<p>There is a whole repository for <a target="_blank" href="https://github.com/appwrite/sdk-generator">Appwrite SDK generator</a> that helps <strong>any</strong> developer prepare SDK for <strong>any</strong> programming language. This tool helps keep all SDKs up to date with Appwrite features and easily fixable if there is some new error introduced by a new language version.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300646974/ui1zO1u3V.gif" alt="GIF about sdks" /></p>
<p>Let's play a game 🎲 Can you figure out the coding language from a tiny snippet of Appwrite code?</p>
<hr />
<pre><code class="lang-javascript"><span class="hljs-comment">// #1</span>
<span class="hljs-keyword">let</span> executionPromise = sdk.functions.createExecution(<span class="hljs-string">"[FUNCTION_ID]"</span>, { 
  <span class="hljs-attr">paymentType</span>: <span class="hljs-string">"onetime"</span>
});
</code></pre>
<p>{% spoiler Answer #1: %}
Javascript? Node? Deno? All of them!
{% endspoiler %}</p>
<hr />
<pre><code class="lang-dart"><span class="hljs-comment">// #2</span>
Future result = account.create(
  email: <span class="hljs-string">'email@example.com'</span>,
  password: <span class="hljs-string">'password'</span>,
);
</code></pre>
<p>{% spoiler Answer #2: %}
Dart, Flutter!
{% endspoiler %}</p>
<pre><code class="lang-java"><span class="hljs-comment">// #3</span>
val avatars = Avatars(client)
GlobalScope.launch {
  val result = avatars.getQR(
    text = <span class="hljs-string">"https://appwrite.io/"</span>,
  )
   println(result); <span class="hljs-comment">// Resource URL        </span>
}
</code></pre>
<p>{% spoiler Answer #3: %}
Java, Kotlin!
{% endspoiler %}</p>
<hr />
<pre><code class="lang-php"><span class="hljs-comment">// #4</span>
$health = <span class="hljs-keyword">new</span> Health($client);
$result = $health-&gt;getTime();
</code></pre>
<p>{% spoiler Answer #4: %}
PHP!
{% endspoiler %}</p>
<hr />
<pre><code class="lang-python"><span class="hljs-comment"># #5</span>
<span class="hljs-keyword">from</span> appwrite.client <span class="hljs-keyword">import</span> Client
<span class="hljs-keyword">from</span> appwrite.services.locale <span class="hljs-keyword">import</span> Locale

client = Client()
locale = Locale(client)
result = locale.get()
</code></pre>
<p>{% spoiler Answer #5: %}
Python
{% endspoiler %}</p>
<hr />
<pre><code class="lang-ruby"><span class="hljs-comment"># #6</span>
functions = Appwrite::Functions.new(client);

response = functions.get_tag(<span class="hljs-symbol">function_id:</span> <span class="hljs-string">'[FUNCTION_ID]'</span>, <span class="hljs-symbol">tag_id:</span> <span class="hljs-string">'[TAG_ID]'</span>);
</code></pre>
<p>{% spoiler Answer #6: %}
Ruby
{% endspoiler %}</p>
<hr />
<pre><code class="lang-bash">appwrite <span class="hljs-built_in">functions</span> listTags --functionId=<span class="hljs-string">"[FUNCTION_ID]"</span> --search=<span class="hljs-string">"[SEARCH]"</span> --<span class="hljs-built_in">limit</span>=<span class="hljs-string">"0"</span> --offset=<span class="hljs-string">"0"</span> --orderType=<span class="hljs-string">"ASC"</span>
</code></pre>
<p>{% spoiler Answer #7: %}
CLI
{% endspoiler %}</p>
<hr />
<p>Did you answer them all correctly? Maybe you didn't? It's fine! We are learning something new every day. Everyone is. Either way, I've got a joke for you as a reward!
{% spoiler Do you know why this article only has eight points and not ten? %}
Because it's a matter of time when Appwrite trips and it will become:
<strong>♾ reasons to fall in ❤️ with Appwrite</strong> 🤯</p>
<p>{% endspoiler %}</p>
<hr />
<p>That's it! I hope this article helped you understand why I fell in ❤️ with Appwrite. Fun fact, you just read the word <code>Appwrite</code> <strong>over 40 times</strong>. Appwrite! If you have any questions, feel free to join Appwrite's Discord server and chat with their amazing community: https://appwrite.io/discord</p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300649276/E4BpW85lK.png" alt="Graphics made by SABO Design" /></p>
<p></p>Cover made by SABO Design.
🔗 <a target="_blank" href="https://www.facebook.com/akorandordesigns">SABO Design</a><p></p>
]]></content:encoded></item><item><title><![CDATA[Appwrite with Angular SSR]]></title><description><![CDATA[Thanks to @Caryntjen from Discord for bringing this problem up

Table Of Contents

Introduction
Setup Angular project with Appwrite
Add Angular Universal to our project
Connect Appwrite to Angular Universal


Introduction 
Server-side rendering can h...]]></description><link>https://blog.matejbaco.eu/appwrite-with-angular-ssr-1</link><guid isPermaLink="true">https://blog.matejbaco.eu/appwrite-with-angular-ssr-1</guid><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Tue, 20 Jul 2021 10:05:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300665989/J5s5AWksu.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Thanks to <code>@Caryntjen</code> from Discord for bringing this problem up</p>
</blockquote>
<h2 id="table-of-contents">Table Of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#introduction">Introduction</a></li>
<li><a class="post-section-overview" href="#setup-angular-project-with-appwrite">Setup Angular project with Appwrite</a></li>
<li><a class="post-section-overview" href="#add-angular-universal-to-our-project">Add Angular Universal to our project</a></li>
<li><a class="post-section-overview" href="#connect-appwrite-to-angular-universal">Connect Appwrite to Angular Universal</a></li>
</ul>
<hr />
<h2 id="introduction-lessa-nameintroductiongreaterlessagreater">Introduction <a></a></h2>
<p>Server-side rendering can help your website speed up the initial load and let bots access your dynamic data to improve SEO. This article will show you how to quickly solve a problem with Appwrite data not being load before rendering a page server-side.</p>
<p>To solve our problem, we will use the library <code>angular-zen</code>. This library will create a zone.js task under the hood, and that helps Angular Universal understand your async code. To learn more about this, you can visit their docs: <a target="_blank" href="https://bs-angular-zen.web.app/docs/zen/injectables/RouteAware.html#resolveInMacroTask">Angular zen docs</a></p>
<blockquote>
<p>In SSR, the server doesn't wait for the async code to complete. The result is scrapers and search engines receiving a page without resolved data, which is bad in case you need them to read some resolved metadata tags for example. Use resolveInMacroTask() to have your server block and wait for resolves before rendering.</p>
</blockquote>
<h2 id="setup-angular-project-with-appwrite-lessa-namesetup-angular-project-with-appwritegreaterlessagreater">Setup Angular project with Appwrite <a></a></h2>
<p>Before solving the problem, let's see the problem! We start by creating an empty angular project:</p>
<pre><code class="lang-bash">ng new appwrite-ssr
<span class="hljs-built_in">cd</span> appwrite-ssr
</code></pre>
<pre><code>? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> enforce stricter <span class="hljs-keyword">type</span> checking <span class="hljs-keyword">and</span> stricter bundle budgets <span class="hljs-keyword">in</span> the workspace?
  This setting helps improve maintainability <span class="hljs-keyword">and</span> catch bugs ahead <span class="hljs-keyword">of</span> <span class="hljs-type">time</span>.
  <span class="hljs-keyword">For</span> more information, see https://angular.io/<span class="hljs-keyword">strict</span> Yes
? Would you <span class="hljs-keyword">like</span> <span class="hljs-keyword">to</span> <span class="hljs-keyword">add</span> Angular routing? Yes
? Which stylesheet <span class="hljs-keyword">format</span> would you <span class="hljs-keyword">like</span> <span class="hljs-keyword">to</span> use? CSS
</code></pre><blockquote>
<p>Make sure to pick the same options to have the same results</p>
</blockquote>
<p>Now, let's write some Appwrite code.  To use Appwrite in our frontend project, we need to install its client-side SDK:</p>
<pre><code class="lang-bash">npm i appwrite
</code></pre>
<p>This is a simple javascript/typescript library with no connection to Angular, so we don't need to worry about importing modules or injecting services. For simplicity, we will do everything in our <code>app.component</code>. Still, it is strongly recommended to put all Appwrite logic into a separate <code>appwrite.service</code> to share data across multiple components in an actual project easily.</p>
<p>Our <code>app.component.ts</code> should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { Appwrite } <span class="hljs-keyword">from</span> <span class="hljs-string">'appwrite'</span>;
<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-root'</span>,
  templateUrl: <span class="hljs-string">'./app.component.html'</span>,
  styleUrls: [<span class="hljs-string">'./app.component.css'</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppComponent <span class="hljs-keyword">implements</span> OnInit {
  title = <span class="hljs-string">'appwrite-ssr'</span>;
  currencies: <span class="hljs-built_in">any</span>; <span class="hljs-comment">// Do not use any in real project</span>

  <span class="hljs-keyword">async</span> ngOnInit() {
    <span class="hljs-keyword">let</span> sdk = <span class="hljs-keyword">new</span> Appwrite();

    sdk
      .setEndpoint(<span class="hljs-string">'https://server.matejbaco.eu/v1'</span>) <span class="hljs-comment">// Your API Endpoint</span>
      .setProject(<span class="hljs-string">'60f2fb6e92712'</span>); <span class="hljs-comment">// Your project ID</span>

    <span class="hljs-comment">// Load currencies from appwrite</span>
    <span class="hljs-keyword">const</span> appwriteCurrencies = <span class="hljs-keyword">await</span> sdk.locale.getCurrencies();

    <span class="hljs-comment">// Store the result into component variable for use in HTML code</span>
    <span class="hljs-built_in">this</span>.currencies = appwriteCurrencies;
  }
}
</code></pre>
<p>First, we imported Appwrite SDK using <code>import { Appwrite } from 'appwrite';</code>. Then, inside <code>ngOnInit</code> we initialized a new instance of the SDK that is connected to our Appwrite server. Finally, we load a list of currencies from Appwrite and store it into a variable to use in HTML code.</p>
<p>Let's switch to <code>app.component.html</code>. This is our code:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Total currencies:<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

<span class="hljs-comment">&lt;!-- We don't have data yet, loading... --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span> *<span class="hljs-attr">ngIf</span>=<span class="hljs-string">"!currencies"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

<span class="hljs-comment">&lt;!-- Data loaded, let's count them --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span> *<span class="hljs-attr">ngIf</span>=<span class="hljs-string">"currencies"</span>&gt;</span>Total: {{ currencies.sum }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>We simply write two blocks of code - one for when data is not loaded yet, one after the loading is finished. Now, if we run <code>ng serve</code> and visit <code>http://localhost:4200/</code>, we can see the currencies being loaded successfully:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300655576/dHJYbDBJg.png" alt="image" /></p>
<p>What about server-side rendering? Let's see... If we look at the source code of our application, we can this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300657346/sSOEdpni3.png" alt="image" /></p>
<p>No useful data for bots! Let's fix that.</p>
<h2 id="add-angular-universal-to-our-project-lessa-nameadd-angular-universal-to-our-projectgreaterlessagreater">Add Angular Universal to our project <a></a></h2>
<p>To prepare our project for server-side rendering, we need to add a new Angular library. Let's stop our Angular development server and run <code>ng add @nguniversal/express-engine</code>. Then, we can run <code>npm run dev:ssr</code> to have the same development server running, but this time with server-side rendering. Let's see what our website looks like to bots now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300659469/YyM81sDVpx.png" alt="image" /></p>
<p>This is awesome, one step at a time! Our Angular code is being rendered properly because we can see our title <code>Total currencies:</code>. We are not done yet, because this pre-rendered HTML does not include our Appwrite data. Instead, we can see <code>...</code>. </p>
<h2 id="connect-appwrite-to-angular-universal-lessa-nameconnect-appwrite-to-angular-universalgreaterlessagreater">Connect Appwrite to Angular Universal <a></a></h2>
<p>As mentioned initially, we will use a library that will help us run the task server-side. To do this, we stop our development server and run <code>npm i @bespunky/angular-zen</code>. Once the library is installed, let's start the development server with <code>npm run dev:ssr</code>.</p>
<p>Angular zen is an Angular library, so we need to add it into <code>imports</code> of our module for it to work properly. To do this, we go into <code>app.module.ts</code> and add add <code>RouterXModule</code> as an import. The module should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NgModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { BrowserModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/platform-browser'</span>;
<span class="hljs-keyword">import</span> { RouterXModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@bespunky/angular-zen/router-x'</span>;
<span class="hljs-keyword">import</span> { AppRoutingModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app-routing.module'</span>;

<span class="hljs-keyword">import</span> { AppComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.component'</span>;

<span class="hljs-meta">@NgModule</span>({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: <span class="hljs-string">'serverApp'</span> }),
    AppRoutingModule,
    RouterXModule.forRoot(),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
</code></pre>
<blockquote>
<p>My IDE noticed the error <code>Appears in the NgModule.imports of AppServerModule, but itself has errors</code>, but I ignored it, and it got resolved by Angular re-building the app. If you can see this error, simply restart the development server, and you should be good to go.</p>
</blockquote>
<p>We need to use <code>RouteAware</code> class in our <code>app.component.ts</code> because we need to access its <code>resolveInMacroTask()</code> method. To do that, we can make our component extend <code>RouteAware</code>. Then we wrap our async code in <code>ngOnInit</code> into <code>resolveInMacroTask</code> and await its result as a promise. Our code will look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RouteAware } <span class="hljs-keyword">from</span> <span class="hljs-string">'@bespunky/angular-zen/router-x'</span>;
<span class="hljs-keyword">import</span> { Appwrite } <span class="hljs-keyword">from</span> <span class="hljs-string">'appwrite'</span>;
<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-root'</span>,
  templateUrl: <span class="hljs-string">'./app.component.html'</span>,
  styleUrls: [<span class="hljs-string">'./app.component.css'</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppComponent <span class="hljs-keyword">extends</span> RouteAware <span class="hljs-keyword">implements</span> OnInit {
  title = <span class="hljs-string">'appwrite-ssr'</span>;
  currencies: <span class="hljs-built_in">any</span>; <span class="hljs-comment">// Do not use any in real project</span>

  <span class="hljs-keyword">async</span> ngOnInit() {
    <span class="hljs-keyword">let</span> sdk = <span class="hljs-keyword">new</span> Appwrite();

    sdk
      .setEndpoint(<span class="hljs-string">'https://server.matejbaco.eu/v1'</span>) <span class="hljs-comment">// Your API Endpoint</span>
      .setProject(<span class="hljs-string">'60f2fb6e92712'</span>); <span class="hljs-comment">// Your project ID</span>

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.resolveInMacroTask(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-comment">// Load currencies from appwrite</span>
      <span class="hljs-keyword">const</span> appwriteCurrencies = <span class="hljs-keyword">await</span> sdk.locale.getCurrencies();

      <span class="hljs-comment">// Store the result into component variable for use in HTML code</span>
      <span class="hljs-built_in">this</span>.currencies = appwriteCurrencies;
    }).toPromise();
  }
}
</code></pre>
<p>We are good to go! Let's see it in action. If I visit our page, I can see the data:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300662003/1w2iFGMCX.png" alt="image" /></p>
<p>If I look at the source code of pre-render, I can see the data too!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300664472/aUbvygwWes.png" alt="image" /></p>
<hr />
<p>That's it! I hope this article helped you to use Appwrite with Angular Universal. If you have any questions, feel free to join Appwrite's Discord server and chat with their amazing community: https://appwrite.io/discord</p>
]]></content:encoded></item><item><title><![CDATA[Backing up Appwrite to Backblaze]]></title><description><![CDATA[Table Of Contents

Introduction
Getting Backblaze secrets
B2 CLI basics
Appwrite MariaDB 👉 Backblaze
Appwrite Docker Volumes 👉 Backblaze


Introduction 
Recently, Appwrite has released an article on how to backup and restore all data on your produc...]]></description><link>https://blog.matejbaco.eu/backing-up-appwrite-to-backblaze-1</link><guid isPermaLink="true">https://blog.matejbaco.eu/backing-up-appwrite-to-backblaze-1</guid><dc:creator><![CDATA[Matej Bačo]]></dc:creator><pubDate>Sat, 17 Jul 2021 10:28:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627300670593/p39rz3S7i.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="table-of-contents">Table Of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#introduction">Introduction</a></li>
<li><a class="post-section-overview" href="#getting-backblaze-secrets">Getting Backblaze secrets</a></li>
<li><a class="post-section-overview" href="#b2-cli-basics">B2 CLI basics</a></li>
<li><a class="post-section-overview" href="#appwrite-mariadb-to-backblaze">Appwrite MariaDB 👉 Backblaze</a></li>
<li><a class="post-section-overview" href="#introduction">Appwrite Docker Volumes 👉 Backblaze</a></li>
</ul>
<hr />
<h2 id="introduction-lessa-nameintroductiongreaterlessagreater">Introduction <a></a></h2>
<p>Recently, Appwrite has released an article on how to backup and restore all data on your production server. You can read it here:
{% post https://dev.to/appwrite/appwrite-in-production-backups-and-restores-4beg %}</p>
<p>This article got me thinking 🤔 Sure, you can backup most MySQL databases into a simple <code>backup.sql</code> file, but what if you have multiple massive projects inside Appwrite? Your disk storage may or may not be able to hold a backup of all Appwrite volumes. Additionally, you most likely need to backup periodically and need multiple versions of the backup. So, what are your options?</p>
<ul>
<li>Write a complex bash script to keep last three versions</li>
<li>Throw all backups to a storage provider and let them take care of it</li>
</ul>
<p>You should go with storage providers not only because they have a retention system already in place, also due to their experience in the field as they most likely care about security a lot and use raids to ensure your data won't get lost.</p>
<p>I wrote this article as I was implementing this on my own production server, and I will show you everything you need to know about backing up <strong>Appwrite</strong> into <strong>Backblaze</strong>.</p>
<h2 id="getting-backblaze-secrets-lessa-namegetting-backblaze-secretsgreaterlessagreater">Getting Backblaze secrets <a></a></h2>
<p>First things first, what is Backblaze? Backblaze is one of the most, if not most, used commercial platform to backup your data for the lowest price possible. They provide solutions to backup your personal computer or your business data. The service we will be using is <strong>B2 Cloud Storage</strong> as this gives our flexibility just like an actual hard drive. You don't have to worry about pricing yet because their free plan gives you 10GB of free space. To learn more about this product and its pricing, you can take a look at: https://www.backblaze.com/b2/cloud-storage.html</p>
<blockquote>
<p>"Over one exabyte of data storage under management, and counting." - Backblaze website</p>
</blockquote>
<p>After creating an account and logging in (make sure to confirm your email), Backblaze may ask you to set up 2FA using your phone number for additional security.</p>
<p>To use Backblaze from our server, we will need:</p>
<ul>
<li>BUCKET_NAME</li>
<li>ACCESS_KEY_ID</li>
<li>APPLICATION_KEY</li>
</ul>
<p>Backblaze uses <strong>buckets</strong> to store your data. In our case, we will only need one bucket as we will only back up two or three files at most (MySQL database and Appwrite Docker volumes). To create a bucket, navigate to <code>Bucket</code> and press <code>Create a Bucket</code>. Give it some name such as <code>appwrite</code> and click <code>Create a Bucket</code> again. You should see a newly created bucket on your list. Make sure to mark your <code>Bucket Name</code> as we will need this later.</p>
<p>If you enter <code>App Keys</code>, you can <code>Add a New Application Key</code>. For simplicity, we will ignore that, and we will use our master key to have all permissions. Simply click <code>Generate a New Master Application Key</code>, and this should give you <code>keyID</code> and <code>applicationKey</code>.</p>
<p>Wohoo 🎉 we have all secrets we need; let's continue!</p>
<h2 id="b2-cli-basics-lessa-nameb2-cli-basicsgreaterlessagreater">B2 CLI basics <a></a></h2>
<p>To make sure everything is working, let's try some simple action on B2:</p>
<ul>
<li>List files in a bucket</li>
<li>Upload a file</li>
<li>List files again</li>
</ul>
<p>We need to have B2 CLI installed on our machine, but this can be easily avoided thanks to a Docker image <code>mtronix/b2-cli:0.0.1</code>. This is the only image I could find that was actually working.</p>
<p>To list all files in our bucket, we run:</p>
<pre><code>docker run \
  --rm \
  mtronix/b2-cli:0.0.1 \
  bash -c \
  "b2 authorize-account <span class="hljs-tag">&lt;<span class="hljs-name">ACCESS_KEY_ID</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">ACCESS_KEY_ID</span>&gt;</span> &amp;&amp; b2 list-file-names <span class="hljs-tag">&lt;<span class="hljs-name">BUCKET_NAME</span>&gt;</span>"
</code></pre><p>The output should be an empty list of files:</p>
<pre><code>{
  <span class="hljs-attr">"files"</span>: [],
  <span class="hljs-attr">"nextFileName"</span>: <span class="hljs-literal">null</span>
}
</code></pre><p>Let's upload a file 😎
We need to have a file... I downloaded a cute cat image and saved it as <code>cat.png</code>. To upload the file, I run:</p>
<pre><code>docker run \
  --rm \
  -v $PWD<span class="hljs-symbol">:/b2</span> \
  mtronix/b2-<span class="hljs-symbol">cli:</span><span class="hljs-number">0</span>.<span class="hljs-number">0</span>.<span class="hljs-number">1</span> \
  bash -c \
  <span class="hljs-string">"b2 authorize-account &lt;ACCESS_KEY_ID&gt; &lt;ACCESS_KEY_ID&gt; &amp;&amp; b2 upload-file &lt;BUCKET_NAME&gt; cat.png cat.png"</span>
</code></pre><p>The successful result should look something like:</p>
<pre><code>{
  <span class="hljs-attr">"action"</span>: <span class="hljs-string">"upload"</span>,
  <span class="hljs-attr">"fileId"</span>: <span class="hljs-string">"4_z78da5cd2a05db73574a90515_f11841831f8e91ca6_d20210717_m092603_c002_v0001140_t0056"</span>,
  <span class="hljs-attr">"fileName"</span>: <span class="hljs-string">"cat.png"</span>,
  <span class="hljs-attr">"size"</span>: <span class="hljs-number">136021</span>,
  <span class="hljs-attr">"uploadTimestamp"</span>: <span class="hljs-number">1626513963000</span>
}
</code></pre><p>Finally, run the <code>list-file-names</code> command from above again, and you should see your file in the array of files:</p>
<pre><code>{
  <span class="hljs-string">"files"</span>: [
    {
      <span class="hljs-string">"accountId"</span>: <span class="hljs-string">"8ac20d754955"</span>,
      <span class="hljs-string">"action"</span>: <span class="hljs-string">"upload"</span>,
      <span class="hljs-string">"bucketId"</span>: <span class="hljs-string">"78da5cd2a05db73574a90515"</span>,
      <span class="hljs-string">"contentLength"</span>: <span class="hljs-number">136021</span>,
<span class="hljs-keyword">...</span>
</code></pre><p>Backblaze let you do all of this function manually on their website. Exercise for you 😲 Remove our test file from the bucket. Do it however you want - you can remove it using B2 CLI or manually on the website.</p>
<h2 id="appwrite-mariadb-backblaze-lessa-nameappwrite-mariadb-to-backblazegreaterlessagreater">Appwrite MariaDB 👉 Backblaze <a></a></h2>
<p>To create an Appwrite MariaDB backup, you run this command:</p>
<pre><code>docker-compose exec mariadb sh -<span class="hljs-built_in">c</span> 'exec mysqldump --all-databases --add-drop-database -u <span class="hljs-string">"$MYSQL_USER"</span> -p <span class="hljs-string">"$MYSQL_ROOT_PASSWORD"</span>' &gt; ./<span class="hljs-built_in">dump</span>.sql
</code></pre><p>(Source: Appwrite article mentioned earlier)</p>
<p>To backup this export into Backblaze, I created a script that:</p>
<ol>
<li>Export MariaDB data to <code>dump.sql</code></li>
<li>Upload <code>dump.sql</code> to Backblaze</li>
<li>Remove <code>dump.sql</code> to free up the space on the machine</li>
</ol>
<p>This is what the script looks like:</p>
<pre><code>docker-compose exec \
  mariadb \
  sh -<span class="hljs-built_in">c</span> \
  'exec mysqldump --all-databases --add-drop-database -u<span class="hljs-string">"$MYSQL_USER"</span> -p<span class="hljs-string">"$MYSQL_ROOT_PASSWORD"</span>' &gt; ./<span class="hljs-built_in">dump</span>.sql ; \
  docker run \
  --rm \
  -v $<span class="hljs-type">PWD</span>:/b2 \
  mtronix/b2-cli:<span class="hljs-number">0.0</span>.<span class="hljs-number">1</span> \
  bash -<span class="hljs-built_in">c</span> \
  <span class="hljs-string">"b2 authorize-account &lt;ACCESS_KEY_ID&gt; &lt;ACCESS_KEY_ID&gt; &amp;&amp; b2 upload-file &lt;BUCKET_NAME&gt; dump.sql dump.sql"</span> ; \
  rm <span class="hljs-built_in">dump</span>.sql
</code></pre><p>Each time you run this script, it will backup your MariaDB database into Backblaze. Backblaze can store multiple versions of a single file; just make sure to configure retention on a bucket, for example, 7 days.</p>
<h2 id="appwrite-docker-volumes-backblaze-lessa-nameappwrite-docker-volumes-to-backblazegreaterlessagreater">Appwrite Docker Volumes 👉 Backblaze <a></a></h2>
<p>Once again, let's get our base command from Appwrite's article:</p>
<p><strong>Before running these commands is it highly recommended to shut down your Appwrite instance to ensure you get a complete backup.</strong></p>
<pre><code><span class="hljs-attribute">mkdir</span> -p backup &amp;&amp; docker run --rm --volumes-from <span class="hljs-string">"$(docker-compose ps -q appwrite)"</span> -v <span class="hljs-variable">$PWD</span>/backup:/backup ubuntu bash -c <span class="hljs-string">"cd /storage/uploads &amp;&amp; tar cvf /backup/uploads.tar ."</span>
</code></pre><pre><code><span class="hljs-attribute">mkdir</span> -p backup &amp;&amp; docker run --rm --volumes-from <span class="hljs-string">"$(docker-compose ps -q appwrite)"</span> -v <span class="hljs-variable">$PWD</span>/backup:/backup ubuntu bash -c <span class="hljs-string">"cd /storage/functions &amp;&amp; tar cvf /backup/functions.tar ."</span>
</code></pre><p>These two commands will backup two different volumes. The first one back up all files in Appwrite storage while the second one backup your function tags. Let's start with backing up uploads.</p>
<p>Just like with MariaDB, we will:</p>
<ol>
<li>Create a backup in <code>backup/uploads.tar</code></li>
<li>Send the file to Backblaze</li>
<li>Remove the backup file</li>
</ol>
<p>The full command to do this is:</p>
<pre><code>mkdir -p backup &amp;&amp; docker run \
  --rm \
  --volumes-from <span class="hljs-string">"$(docker-compose ps -q appwrite)"</span> \
  -v $PWD/<span class="hljs-symbol">backup:</span>/backup \
  ubuntu \
  bash -c \
  <span class="hljs-string">"cd /storage/uploads &amp;&amp; tar cvf /backup/uploads.tar ."</span>  ; \
  docker run \
  --rm \
  -v $PWD<span class="hljs-symbol">:/b2</span> \
  mtronix/b2-<span class="hljs-symbol">cli:</span><span class="hljs-number">0</span>.<span class="hljs-number">0</span>.<span class="hljs-number">1</span> \
  bash -c \
  <span class="hljs-string">"b2 authorize-account &lt;ACCESS_KEY_ID&gt; &lt;ACCESS_KEY_ID&gt; &amp;&amp; b2 upload-file &lt;BUCKET_NAME&gt; backup/uploads.tar upload_backup.tar"</span> ; \
  rm backup/uploads.tar
</code></pre><p>This should create a file called <code>upload_backup.tar</code> in your Backblaze bucket.</p>
<p>Exercise time 💪 You will need to backup Appwrite functions too! Using all knowledge from this article, you should be able to prepare a command that will do it.
If you are here only to copy&amp;paste commands, here is the command:
{% spoiler Command to backup Appwrite function to Backblaze %}</p>
<pre><code>mkdir -p backup &amp;&amp; docker run \
  --rm \
  --volumes-from <span class="hljs-string">"$(docker-compose ps -q appwrite)"</span> \
  -v $PWD/<span class="hljs-symbol">backup:</span>/backup \
  ubuntu \
  bash -c \
  <span class="hljs-string">"cd /storage/functions &amp;&amp; tar cvf /backup/functions.tar ."</span>  ; \
  docker run \
  --rm \
  -v $PWD<span class="hljs-symbol">:/b2</span> \
  mtronix/b2-<span class="hljs-symbol">cli:</span><span class="hljs-number">0</span>.<span class="hljs-number">0</span>.<span class="hljs-number">1</span> \
  bash -c \
  <span class="hljs-string">"b2 authorize-account &lt;ACCESS_KEY_ID&gt; &lt;ACCESS_KEY_ID&gt; &amp;&amp; b2 upload-file &lt;BUCKET_NAME&gt; backup/functions.tar upload_functions.tar"</span> ; \
  rm backup/functions.tar
</code></pre><p>{% endspoiler %}</p>
<hr />
<p>That's it! I hope this article helped you backing up your Appwrite server. If you have <strong>any</strong> questions, feel free to join Appwrite's Discord server and chat with their <strong>amazing</strong> community: https://appwrite.io/discord</p>
]]></content:encoded></item></channel></rss>