<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Graita Sukma Febriansyah Triwildan Azmi</title>
    <description>The latest articles on DEV Community by Graita Sukma Febriansyah Triwildan Azmi (@wildanzrrrr).</description>
    <link>https://dev.to/wildanzrrrr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F918874%2F2314cecf-33cb-4638-b4d7-b14628dcddbc.jpg</url>
      <title>DEV Community: Graita Sukma Febriansyah Triwildan Azmi</title>
      <link>https://dev.to/wildanzrrrr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wildanzrrrr"/>
    <language>en</language>
    <item>
      <title>So many types of social engineering hacks, targeting web3 developers 💀</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Fri, 27 Feb 2026 16:47:30 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/so-many-types-of-social-engineering-hacks-targeting-web3-developers-21cn</link>
      <guid>https://dev.to/wildanzrrrr/so-many-types-of-social-engineering-hacks-targeting-web3-developers-21cn</guid>
      <description>&lt;p&gt;Picture this.&lt;br&gt;
You’re chilling, shipping features, maybe fixing a random “gas estimation failed” bug for the 4th time this day.&lt;/p&gt;

&lt;p&gt;Then a DM lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Hey, we’re doing an NFT project and need someone like you to help us.”&lt;/li&gt;
&lt;li&gt;“Loved your GitHub profile. I see a lot of contribution you made. Do you want to contribute in our project?”&lt;/li&gt;
&lt;li&gt;“Your repo is vulnerable. Here’s a PR to fix it.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because you’re a builder, you do the builder thing:&lt;/p&gt;

&lt;p&gt;You clone, run, click, and merge.&lt;/p&gt;

&lt;p&gt;Congratulations, you just got socially engineered.&lt;/p&gt;

&lt;p&gt;Alright, shall we begin :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Web3 devs get targeted so hard
&lt;/h2&gt;

&lt;p&gt;Short answer: because you don’t just have the code, you have the access!&lt;/p&gt;

&lt;p&gt;Something like: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deployer keys (or the machine that touches them)&lt;/li&gt;
&lt;li&gt;CI secrets (that can publish packages, push image)&lt;/li&gt;
&lt;li&gt;priviledged social media like Discord/Telegram that can “announce”&lt;/li&gt;
&lt;li&gt;and so on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They can just break your workflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In Web3, the fastest way to drain funds is not a re-entrancy bug. It’s a developer having a normal day.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The menu of attacks (Web3 dev edition)
&lt;/h2&gt;

&lt;p&gt;Below are the patterns I keep seeing — different flavors, same core trick: &lt;strong&gt;get you to execute or approve something you normally wouldn’t.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Fake recruiter → “coding assignment” malware
&lt;/h3&gt;

&lt;p&gt;This one is disgustingly effective because it weaponizes your ambition.&lt;/p&gt;

&lt;p&gt;Flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recruiter reaches out (LinkedIn, Telegram, X, email)&lt;/li&gt;
&lt;li&gt;You get a “take-home task”&lt;/li&gt;
&lt;li&gt;Task includes repo / zip / “run this script”&lt;/li&gt;
&lt;li&gt;You run it locally&lt;/li&gt;
&lt;li&gt;Your machine leaks secrets / tokens / wallet material / browser data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Security teams have been tracking multiple waves of this style of campaign, including variants attributed to DPRK-linked activity. ReversingLabs described a “fake recruiter” branch targeting JS/Python developers with crypto-themed tasks (active since &lt;strong&gt;May 2025&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Google’s threat intel also described DPRK activity abusing the &lt;em&gt;job interview process&lt;/em&gt; as the delivery mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “It’s just code, I can inspect it.”&lt;/p&gt;

&lt;p&gt;Yeah… and you still ran it.&lt;/p&gt;




&lt;h3&gt;
  
  
  2) Fake audit / fake security firm / “critical vuln” panic
&lt;/h3&gt;

&lt;p&gt;This one targets &lt;strong&gt;your responsibility reflex&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Common lures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We found a critical issue in your protocol”&lt;/li&gt;
&lt;li&gt;“We’re a security partner of , please validate this quickly”&lt;/li&gt;
&lt;li&gt;“Here’s a PoC / Foundry test / script to reproduce”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payload is often the same: &lt;strong&gt;get you to run something&lt;/strong&gt; (or open a doc) on a machine that has access to important things.&lt;/p&gt;

&lt;p&gt;This vibe rhymes with how the Ronin/Axie compromise was reported: the initial hook wasn’t a smart contract exploit, it was &lt;strong&gt;a human being convinced to open something in a trusted context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “If I ignore this and it’s real, I’m negligent.”&lt;/p&gt;

&lt;p&gt;Attackers love that sentence.&lt;/p&gt;




&lt;h3&gt;
  
  
  3) Malicious npm/PyPI packages hiding inside “normal” dev work
&lt;/h3&gt;

&lt;p&gt;If you’re a Web3 dev, you probably install random packages weekly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wallet utils&lt;/li&gt;
&lt;li&gt;crypto helpers&lt;/li&gt;
&lt;li&gt;ABI tooling&lt;/li&gt;
&lt;li&gt;build plugins&lt;/li&gt;
&lt;li&gt;“quick scripts”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attackers know this.&lt;/p&gt;

&lt;p&gt;Recent reporting and research has highlighted campaigns planting malicious packages and abusing open-source trust — including fake recruiter flows that lead devs to install specific packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “It’s open source, and it has 200 stars.”&lt;/p&gt;

&lt;p&gt;Stars are not a security model.&lt;/p&gt;




&lt;h3&gt;
  
  
  4) GitHub-native attacks: PRs, Actions, token theft, CI secret exfil
&lt;/h3&gt;

&lt;p&gt;This is the grown-up version of phishing.&lt;/p&gt;

&lt;p&gt;Instead of attacking &lt;em&gt;you&lt;/em&gt;, they attack the system you rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a PR that looks legit but sneaks in a postinstall / script&lt;/li&gt;
&lt;li&gt;a compromised maintainer account&lt;/li&gt;
&lt;li&gt;a malicious GitHub Action update&lt;/li&gt;
&lt;li&gt;dependency bumps that pull in the payload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because CI has secrets, &lt;strong&gt;CI is a jackpot&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There have been large-scale supply-chain incidents reported in the npm/GitHub ecosystem where malware focuses on stealing developer secrets and CI/CD credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “CI ran green, so it’s safe.”&lt;/p&gt;

&lt;p&gt;CI can be the victim too.&lt;/p&gt;




&lt;h3&gt;
  
  
  5) Discord/Telegram “verification” bots and community impersonation
&lt;/h3&gt;

&lt;p&gt;Web3 teams live in chat apps. Attackers live there too.&lt;/p&gt;

&lt;p&gt;Patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fake “Collab Manager” accounts&lt;/li&gt;
&lt;li&gt;cloned communities&lt;/li&gt;
&lt;li&gt;fake “verify to enter” bots&lt;/li&gt;
&lt;li&gt;fake airdrop/trading groups delivering malware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some threat reports have noted malware distribution and scam flows increasingly happening through Telegram-style channels and “verification” mechanics rather than classic email phishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “It’s in the official group.”&lt;/p&gt;

&lt;p&gt;Ask: official according to &lt;em&gt;who&lt;/em&gt;?&lt;/p&gt;




&lt;h3&gt;
  
  
  6) Wallet drainers &amp;amp; signature tricks (devs are not immune)
&lt;/h3&gt;

&lt;p&gt;Even if you “never share seed phrases,” you can still get wrecked by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signing a malicious message&lt;/li&gt;
&lt;li&gt;approving an unlimited allowance on a compromised dApp&lt;/li&gt;
&lt;li&gt;signing something you didn’t simulate / understand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drainer operations are basically a service economy now (tooling, panels, distribution playbooks).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “It’s just a signature, not a transaction.”&lt;/p&gt;

&lt;p&gt;Sometimes the signature is the whole attack.&lt;/p&gt;




&lt;h3&gt;
  
  
  7) “Decentralized hosting” payloads (harder to takedown)
&lt;/h3&gt;

&lt;p&gt;Some campaigns are experimenting with hiding malicious payload delivery in places that are annoying to block, including techniques that leverage public blockchains as part of infrastructure.&lt;/p&gt;

&lt;p&gt;Google described DPRK-linked activity using &lt;strong&gt;EtherHiding&lt;/strong&gt; to support malware delivery in a way that complicates blocking/takedown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builder trap:&lt;/strong&gt; “We blocked the domain.”&lt;/p&gt;

&lt;p&gt;Cool. The payload isn’t on a normal domain anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  The common mechanic: they’re not stealing your keys — they’re stealing your &lt;em&gt;decisions&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;If you strip the details, most of these attacks are just three moves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Establish legitimacy&lt;/strong&gt; (brand, identity, context)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply urgency&lt;/strong&gt; (“now”, “ASAP”, “critical”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger an action&lt;/strong&gt; (run, install, merge, sign)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the defense is not only “better antivirus.”&lt;/p&gt;

&lt;p&gt;It’s better &lt;em&gt;decision gates&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple decision rule (use this daily)
&lt;/h2&gt;

&lt;p&gt;When someone asks you to do something “urgent,” ask three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; can I verify who you are &lt;em&gt;out of band&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; what exact action do you want me to take?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius:&lt;/strong&gt; what’s the worst-case if this is malicious?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the blast radius includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“leak secrets”&lt;/li&gt;
&lt;li&gt;“publish code”&lt;/li&gt;
&lt;li&gt;“sign something”&lt;/li&gt;
&lt;li&gt;“deploy something”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…then you don’t do it in a DM window. You move it into a controlled process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: attackers ship too
&lt;/h2&gt;

&lt;p&gt;Social engineering attacks targeting Web3 developers are getting “better” for the same reason Web3 products get better:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;people iterate. they learn. they optimize funnels.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So don’t just harden your contracts.&lt;/p&gt;

&lt;p&gt;Harden your &lt;em&gt;habits&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Because in 2026, the easiest exploit is still:&lt;/p&gt;

&lt;p&gt;A developer, a deadline, and one “quick task.”&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it’s helpful 📖. See you in the next article 🙌.&lt;/p&gt;

</description>
      <category>security</category>
      <category>hack</category>
    </item>
    <item>
      <title>From Monolith to Services Without Regret: The Boring Migration Plan</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Thu, 12 Feb 2026 14:55:52 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/from-monolith-to-services-without-regret-the-boring-migration-plan-213l</link>
      <guid>https://dev.to/wildanzrrrr/from-monolith-to-services-without-regret-the-boring-migration-plan-213l</guid>
      <description>&lt;p&gt;The first time I tried to &lt;strong&gt;“go microservices”&lt;/strong&gt;, I did it for the most honest reason developers ever do anything: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Because it sounded like the grown-up move&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our monolith was getting bigger, deploys were slower, codebase more sticky, and a couple of endpoints were hot and fcking slow. One incident turned into a full-team debugging party. And somewhere in my head, &lt;strong&gt;“I think microservices were the obvious answer”&lt;/strong&gt; like upgrading from a single server to real distributed services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fks530fbzq4d97z064259.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fks530fbzq4d97z064259.png" alt="monolith shit, microservice shits" width="640" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I did what everyone does when they’re tired and ambitious, &lt;strong&gt;“started splitting things”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;New repo, new service name, new pipeline, new database, and new clean boundary. For a few days, it felt amazing (like I had finally escaped :D).&lt;/p&gt;

&lt;p&gt;Then reality showed up and punched us hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local dev got harder to set up&lt;/li&gt;
&lt;li&gt;debugging became distributed archeology&lt;/li&gt;
&lt;li&gt;staging became “work in service A but not in service B”&lt;/li&gt;
&lt;li&gt;tracing didn’t exist yet, so everything was “maybe network issue?”&lt;/li&gt;
&lt;li&gt;deployment multiplied&lt;/li&gt;
&lt;li&gt;auth became a mess&lt;/li&gt;
&lt;li&gt;versioning becae a new job&lt;/li&gt;
&lt;li&gt;and much moreeeee&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monolith wasn’t gone, it just moved into the network. That’s when I learned the unpopular truth. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;microservices don't remove complexity, they relocate it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And if you migrate because you’re &lt;strong&gt;“annoyed”&lt;/strong&gt;, you’ll regret it.&lt;/p&gt;

&lt;p&gt;So I rewound. Not to abandon services forever, but to adopt a migration plan that doesn’t destroy your ability to ship. A plan so boring it actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was never the monolith
&lt;/h2&gt;

&lt;p&gt;It was the lack of boundaries inside it. Most monolith pain comes from two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;everything can touch everything &lt;/li&gt;
&lt;li&gt;changes are not isolated&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the goal isn’t &lt;strong&gt;“split into services”&lt;/strong&gt;. The goal is make boundaries and only then decide what deserves to become a service. If you can’t draw boundaries in a monolith, you won’t magically draw them across repos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuwlweih59k549z6rhh98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuwlweih59k549z6rhh98.png" alt="Explaining microservices" width="800" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring plan: 6 steps that avoid regret
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Make the monolith modular first (a modular monolith)
&lt;/h3&gt;

&lt;p&gt;Before I extracted anything, I forced structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate modules by domain (not by folder vibes)&lt;/li&gt;
&lt;li&gt;enforce boundaries (imports, layering rules)&lt;/li&gt;
&lt;li&gt;stop sharing random utilities everywhere&lt;/li&gt;
&lt;li&gt;define what data each module owns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sounds like a refactor that pays rent.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Pick the first services based on pain and isolation
&lt;/h3&gt;

&lt;p&gt;Most teams pick services based on what’s &lt;strong&gt;“important”&lt;/strong&gt;. I picked based on what’s &lt;strong&gt;isolatable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good first candidates usually have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clear inputs/outputs&lt;/li&gt;
&lt;li&gt;fewer dependencies&lt;/li&gt;
&lt;li&gt;less shared data&lt;/li&gt;
&lt;li&gt;high traffic or distinct scaling needs&lt;/li&gt;
&lt;li&gt;a team that can own it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad first candidates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“core domain logic” touching everything&lt;/li&gt;
&lt;li&gt;workflows that span the entire product&lt;/li&gt;
&lt;li&gt;anything that requires shared transactions across many tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first extraction attempt failed because I picked a “central” domain.&lt;/p&gt;




&lt;h3&gt;
  
  
  3) &lt;strong&gt;Use the Strangler pattern: route traffic gradually&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of cutting over in one dramatic weekend, I did this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep monolith endpoint as the “front door”&lt;/li&gt;
&lt;li&gt;route a small percent of traffic to the new service&lt;/li&gt;
&lt;li&gt;ramp up gradually&lt;/li&gt;
&lt;li&gt;keep a kill switch to fall back to monolith fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This changed the migration from “big bang” to “progressive rollout.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You’re not migrating code. You’re migrating risk.&lt;/p&gt;




&lt;h3&gt;
  
  
  4) &lt;strong&gt;Freeze the old behavior with contract tests&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The easiest way to break things is to rewrite logic with confidence.&lt;/p&gt;

&lt;p&gt;So I wrote contract tests around the monolith behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request/response shapes&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;li&gt;error codes&lt;/li&gt;
&lt;li&gt;expected side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I ran the same tests against the new service.&lt;/p&gt;

&lt;p&gt;This prevented the classic migration bug:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We improved it… and now it behaves differently.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5) &lt;strong&gt;Split data the boring way: keep DB shared at first (sometimes)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This part is controversial, but it saved my migration.&lt;/p&gt;

&lt;p&gt;Instead of going “separate database per service” on day one, I staged it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1:&lt;/strong&gt; service has its own code + deploy + scaling, but reads from the existing DB (carefully).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2:&lt;/strong&gt; introduce a dedicated schema or tables with clear ownership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3:&lt;/strong&gt; move to a separate database only when you have events/replication/ownership clear.&lt;/p&gt;

&lt;p&gt;This avoided building a full distributed data architecture prematurely.&lt;/p&gt;

&lt;p&gt;Because the moment you separate DBs, you inherit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eventual consistency&lt;/li&gt;
&lt;li&gt;dual writes&lt;/li&gt;
&lt;li&gt;eventing&lt;/li&gt;
&lt;li&gt;reconciliation&lt;/li&gt;
&lt;li&gt;backfills&lt;/li&gt;
&lt;li&gt;debugging async flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A service boundary is already a big change.&lt;/p&gt;

&lt;p&gt;A data boundary is the real point of no return.&lt;/p&gt;




&lt;h3&gt;
  
  
  6) &lt;strong&gt;Build the boring platform stuff earlier than you want&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Microservices only feel “clean” when your platform is mature.&lt;/p&gt;

&lt;p&gt;So I had to invest in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;centralized logging&lt;/li&gt;
&lt;li&gt;tracing (or at least correlation IDs)&lt;/li&gt;
&lt;li&gt;consistent authN/authZ strategy&lt;/li&gt;
&lt;li&gt;shared observability dashboards&lt;/li&gt;
&lt;li&gt;deployment automation&lt;/li&gt;
&lt;li&gt;versioning rules between services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise your migration becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We moved to microservices and now we can’t debug anything.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The “services without regret” decision rule
&lt;/h2&gt;

&lt;p&gt;After the boring plan, I changed my criteria.&lt;/p&gt;

&lt;p&gt;A module should become a service only if it needs at least one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;independent scaling&lt;/strong&gt; (traffic/compute profile differs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fault isolation&lt;/strong&gt; (it fails and shouldn’t take everything down)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;team ownership boundaries&lt;/strong&gt; (real org scaling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;security boundaries&lt;/strong&gt; (sensitive workloads)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the reason is “the monolith feels messy,” the fix is not services.&lt;/p&gt;

&lt;p&gt;The fix is boundaries, standards, and refactoring.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w8g9bts7hub00wcf7ki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w8g9bts7hub00wcf7ki.png" alt="Me with microservice" width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The best outcome wasn’t “microservices”
&lt;/h2&gt;

&lt;p&gt;It was &lt;strong&gt;boring deployments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the plan worked, something surprising happened:&lt;/p&gt;

&lt;p&gt;We didn’t feel like we “migrated to microservices.”&lt;/p&gt;

&lt;p&gt;We just slowly stopped suffering.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploys got smaller&lt;/li&gt;
&lt;li&gt;incidents got narrower&lt;/li&gt;
&lt;li&gt;teams gained ownership&lt;/li&gt;
&lt;li&gt;scaling became targeted&lt;/li&gt;
&lt;li&gt;the codebase became less scary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the goal.&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it’s helpful 📖. See you in the next article 🙌&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Definition of Done Is a Lie (Unless It Includes These 5 Things)</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Thu, 12 Feb 2026 03:16:25 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/definition-of-done-is-a-lie-unless-it-includes-these-5-things-12n</link>
      <guid>https://dev.to/wildanzrrrr/definition-of-done-is-a-lie-unless-it-includes-these-5-things-12n</guid>
      <description>&lt;p&gt;I used to think “done” meant: PR merged, CI green, deployed to staging. That’s it. Ship the feature, move on, next ticket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4aez9mxysl1v93ragb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4aez9mxysl1v93ragb4.png" alt="PR Merged" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I shipped a feature that was &lt;em&gt;done&lt;/em&gt;… and immediately turned into a slow-burn production mess.&lt;/p&gt;

&lt;p&gt;Not because the code was “bad” (okay, a bit maybe), but because the &lt;strong&gt;definition&lt;/strong&gt; was bad.&lt;/p&gt;

&lt;p&gt;No dashboard. No kill switch. No clear rollback. No runbook. No clue what was happening when users reported “it’s broken” except a vague spike in errors and a Slack thread that started with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Anyone knows what changed?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s when I realized something brutal:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most Definitions of Done are just “we stopped working on it.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not “it’s safe.” Not “it’s operable.” Not “it’s maintainable.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu519oaz55ujjv4qhbcb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu519oaz55ujjv4qhbcb.png" alt="Done or Complete?" width="225" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here’s what I changed. These are the &lt;strong&gt;5 things&lt;/strong&gt; that finally made “done” mean what it sounds like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;shipped, safe, and survivable.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem was never speed
&lt;/h2&gt;

&lt;p&gt;It was &lt;strong&gt;fragility disguised as velocity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A weak DoD feels productive because it optimizes for one visible milestone: &lt;em&gt;merge&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But the cost doesn’t disappear. It’s just deferred into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on-call chaos&lt;/li&gt;
&lt;li&gt;hotfix releases&lt;/li&gt;
&lt;li&gt;“why is this slow?” investigations&lt;/li&gt;
&lt;li&gt;support tickets you can’t reproduce&lt;/li&gt;
&lt;li&gt;teammates afraid to touch the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s like building a bridge and calling it “done” because the concrete dried—without checking if trucks can cross it.&lt;/p&gt;

&lt;p&gt;So I reframed DoD into one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A feature is done when it can survive production without its author babysitting it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1) Done means &lt;strong&gt;Observable&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If it breaks, can I know &lt;em&gt;what&lt;/em&gt; broke in under 5 minutes?&lt;/p&gt;

&lt;p&gt;When a user says “it’s not working,” the worst response is “I can’t see anything.”&lt;/p&gt;

&lt;p&gt;That’s not a monitoring issue. That’s a definition issue.&lt;/p&gt;

&lt;p&gt;So now, “done” includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structured logs (not vibes)&lt;/li&gt;
&lt;li&gt;request IDs / correlation IDs&lt;/li&gt;
&lt;li&gt;key metrics: success rate, error rate, latency (p50/p95)&lt;/li&gt;
&lt;li&gt;a simple dashboard that answers: &lt;em&gt;is it healthy?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;alerts only when the feature is truly critical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The shift:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I stopped treating observability as “nice to have later.”&lt;/p&gt;

&lt;p&gt;I treat it as part of the feature—like the UI or database schema.&lt;/p&gt;

&lt;p&gt;Because shipping without telemetry is like deploying without SSH access.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Done means &lt;strong&gt;Reversible&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Can I disable it instantly without redeploying and praying?&lt;/p&gt;

&lt;p&gt;This is the “I learned it the hard way” one.&lt;/p&gt;

&lt;p&gt;At some point, every feature will misbehave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;edge case you didn’t predict&lt;/li&gt;
&lt;li&gt;downstream service failing&lt;/li&gt;
&lt;li&gt;traffic spike&lt;/li&gt;
&lt;li&gt;data inconsistency&lt;/li&gt;
&lt;li&gt;unexpected user behavior&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3) Done means &lt;strong&gt;Safe by Default&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If someone tries to misuse it, does it fail safely?&lt;/p&gt;

&lt;p&gt;Many incidents aren’t “bugs.” They’re missing guardrails.&lt;/p&gt;

&lt;p&gt;So “done” includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;proper authorization (not just authentication)&lt;/li&gt;
&lt;li&gt;validation for user inputs (especially file uploads)&lt;/li&gt;
&lt;li&gt;rate limiting / abuse controls if the endpoint is “fun”&lt;/li&gt;
&lt;li&gt;secure defaults (deny &amp;gt; allow)&lt;/li&gt;
&lt;li&gt;secrets not logged, not leaked, not shipped to clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one matters because production isn’t just for users.&lt;/p&gt;

&lt;p&gt;Production is also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bots (AI crawls, those shit)&lt;/li&gt;
&lt;li&gt;weird clients&lt;/li&gt;
&lt;li&gt;accidental spam&lt;/li&gt;
&lt;li&gt;malicious traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Done means it survives real users and real attackers.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Done means &lt;strong&gt;Correct Under Real Conditions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not “works on my machine.” Works in the world.&lt;/p&gt;

&lt;p&gt;Staging is polite. Production is chaos.&lt;/p&gt;

&lt;p&gt;Production has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;unexpected payload sizes&lt;/li&gt;
&lt;li&gt;real traffic patterns&lt;/li&gt;
&lt;li&gt;real data inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So “done” includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;negative tests + edge case tests&lt;/li&gt;
&lt;li&gt;idempotency where duplicates can happen (payments, messaging, webhook handlers)&lt;/li&gt;
&lt;li&gt;bounded retries + timeouts (no infinite retry loops)&lt;/li&gt;
&lt;li&gt;defined behavior when dependencies fail (fallbacks, graceful degradation)&lt;/li&gt;
&lt;li&gt;load assumptions written somewhere (“expected max payload, expected latency”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The shift:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I stopped thinking testing is about correctness.&lt;/p&gt;

&lt;p&gt;Testing is about &lt;em&gt;survivability under pressure.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Done means &lt;strong&gt;Explainable to the Next Person&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If I disappear for a week, can someone operate it without summoning me?&lt;/p&gt;

&lt;p&gt;This is the part devs underestimate because it doesn’t feel technical.&lt;/p&gt;

&lt;p&gt;But it’s the difference between a team that ships and a team that waits.&lt;/p&gt;

&lt;p&gt;So “done” includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a short runbook: how to debug, where to look, common failures&lt;/li&gt;
&lt;li&gt;updated API docs/contracts&lt;/li&gt;
&lt;li&gt;“how to test locally” steps (even minimal)&lt;/li&gt;
&lt;li&gt;clear ownership: who maintains this, which service, which repo&lt;/li&gt;
&lt;li&gt;release notes (short) if user behavior changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What a “real” Definition of Done looks like
&lt;/h2&gt;

&lt;p&gt;Here’s the compact version I wish I had earlier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A feature is “done” when it is:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observable&lt;/strong&gt; (logs/metrics/traces + dashboard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversible&lt;/strong&gt; (feature flag + rollback plan)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe&lt;/strong&gt; (authZ + validation + rate limiting if needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct in production reality&lt;/strong&gt; (idempotency + timeouts + failure handling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explainable&lt;/strong&gt; (runbook + docs + ownership)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The merged code is not done.&lt;/p&gt;

&lt;p&gt;It’s just… merged.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pushback: “This slows us down”
&lt;/h2&gt;

&lt;p&gt;Yeah. It slows down &lt;strong&gt;this PR&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But it speeds up the next 20 releases.&lt;/p&gt;

&lt;p&gt;Because the alternative is paying the same cost later, but with interest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production incidents&lt;/li&gt;
&lt;li&gt;emergency fixes&lt;/li&gt;
&lt;li&gt;broken trust&lt;/li&gt;
&lt;li&gt;morale drain&lt;/li&gt;
&lt;li&gt;tech debt that turns into fear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DoD is not a bureaucracy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s how you buy back your future time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: the best Definition of Done is boring
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nrfdgcdc89nzzfhq8j3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nrfdgcdc89nzzfhq8j3.png" alt="Devs need sleep" width="800" height="908"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the best part? I could finally ship with confidence—not because I became smarter, but because I stopped calling things done before they were operable.&lt;/p&gt;

&lt;p&gt;The goal isn’t perfection.&lt;/p&gt;

&lt;p&gt;The goal is to be able to sleep.&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it’s helpful 📖. See you in the next article 🙌&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>software</category>
      <category>management</category>
    </item>
    <item>
      <title>💀 The Hidden Cost of “AI Features”: What I Learned Turning a Messaging App Into an Expense Tracker</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Wed, 04 Feb 2026 14:59:10 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/the-hidden-cost-of-ai-features-what-i-learned-turning-a-messaging-app-into-an-expense-tracker-2fd1</link>
      <guid>https://dev.to/wildanzrrrr/the-hidden-cost-of-ai-features-what-i-learned-turning-a-messaging-app-into-an-expense-tracker-2fd1</guid>
      <description>&lt;p&gt;I create a Telegram bot and treat it as my personal ledger. It’s a bit addictive because “I can type anything and it just works”. And I realized, just ship it!&lt;/p&gt;

&lt;p&gt;Then my chad's message themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Makan soto 20k.”&lt;/li&gt;
&lt;li&gt;“Indihome 350k”&lt;/li&gt;
&lt;li&gt;“Isi bensin 100k, tambah angin 5k, cilok 10k”&lt;/li&gt;
&lt;li&gt;upload Indomaret photo receipt&lt;/li&gt;
&lt;li&gt;a voice note after leaving a café&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I pivoted the product into what felt obvious: &lt;strong&gt;a chat-first expense tracker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The killer feature was “auto-save expenses” from &lt;strong&gt;text&lt;/strong&gt;, &lt;strong&gt;image&lt;/strong&gt;, and &lt;strong&gt;audio&lt;/strong&gt;. And once data was captured, we added analytics: weekly and monthly breakdowns, categories, trends, and a “you’re going to overshoot” signal (fancy word for overcast, wkwkw).&lt;/p&gt;

&lt;p&gt;The product felt magical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don’t “log expenses”. You just send your saved messages.&lt;br&gt;
The bot does the bookkeeping.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The beta version was cheap because the demo was polite
&lt;/h2&gt;

&lt;p&gt;In demos, users send one clean message. One receipt. One voice note. Everything is short and predictable.&lt;/p&gt;

&lt;p&gt;In real production, a chat UI is basically a cost amplifier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users spam message&lt;/li&gt;
&lt;li&gt;they resend when it feels slow&lt;/li&gt;
&lt;li&gt;they paste long text&lt;/li&gt;
&lt;li&gt;they upload huge images&lt;/li&gt;
&lt;li&gt;they record 2-minute audio notes when 10 seconds would do&lt;/li&gt;
&lt;li&gt;they ask for “weekly summary” five times because it looks fun&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A messaging interface doesn’t behave like a form, it behaves like a stream. And for sure, AI love stream.. because it can charge you for every token spent :D&lt;/p&gt;

&lt;h2&gt;
  
  
  My original pipeline (aka “how I accidentally built a money burner”)
&lt;/h2&gt;

&lt;p&gt;For each message, I did something like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If it’s text&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;detect if it’s an expense&lt;/li&gt;
&lt;li&gt;extract amount, merchant, category, or define it &lt;/li&gt;
&lt;li&gt;save transaction&lt;/li&gt;
&lt;li&gt;respond with a confirmation + a friendly explanation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If it’s an image&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;OCR/vision extraction&lt;/li&gt;
&lt;li&gt;parse line items or totals&lt;/li&gt;
&lt;li&gt;extract amount, merchant, category, or define it &lt;/li&gt;
&lt;li&gt;save transaction&lt;/li&gt;
&lt;li&gt;respond with what it found and ask for confirmation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If it’s an audio&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;transcribe&lt;/li&gt;
&lt;li&gt;run the same extraction as text&lt;/li&gt;
&lt;li&gt;save transaction&lt;/li&gt;
&lt;li&gt;respond with a summary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then on top of that, I had analytics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weekly/monthly charts&lt;/li&gt;
&lt;li&gt;“top categories”&lt;/li&gt;
&lt;li&gt;“spikes” (“why is transport higher this week?”)&lt;/li&gt;
&lt;li&gt;forecasting / &lt;em&gt;overcast&lt;/em&gt; alerts (“you’re trending 18% above your normal pace”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the app wasn’t “AI + database.”&lt;/p&gt;

&lt;p&gt;It was &lt;strong&gt;a distributed system with metered inference&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hidden cost #1: tokens compound in a chat product
&lt;/h2&gt;

&lt;p&gt;I didn’t feel it immediately because my brain was still in “API calls are roughly the same cost” mode.&lt;/p&gt;

&lt;p&gt;But LLM usage isn’t flat. It scales with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt length (system instructions + rules + examples)&lt;/li&gt;
&lt;li&gt;user message length&lt;/li&gt;
&lt;li&gt;retrieved context (history, past expenses, category list)&lt;/li&gt;
&lt;li&gt;output length (confirmations, explanations, summaries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And chat systems naturally push you toward &lt;em&gt;more context&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Use the last 20 messages so it understands the user”&lt;/li&gt;
&lt;li&gt;“Include last month’s spending so it can categorize smarter”&lt;/li&gt;
&lt;li&gt;“Include the user’s recurring merchants”&lt;/li&gt;
&lt;li&gt;“Include the budget settings”&lt;/li&gt;
&lt;li&gt;“Include the currency rules”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly, the simplest “makan soto 20k” message is riding inside a prompt suitcase filled with your entire product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheapest token is the one you never send.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chat UX makes you forget that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hidden cost #2: multimodal is not one cost—it's three
&lt;/h2&gt;

&lt;p&gt;Text-only expense parsing was manageable.&lt;/p&gt;

&lt;p&gt;Images and audio were where the bill started growing teeth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Images (receipts)
&lt;/h3&gt;

&lt;p&gt;A single receipt can mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image upload bandwidth + storage&lt;/li&gt;
&lt;li&gt;vision/OCR inference cost&lt;/li&gt;
&lt;li&gt;parsing errors → retries&lt;/li&gt;
&lt;li&gt;confirmation flows (extra model calls)&lt;/li&gt;
&lt;li&gt;edge cases (blur, shadows, multiple totals, currencies)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And users don’t upload “small receipts.”&lt;/p&gt;

&lt;p&gt;They upload full-res photos straight off their camera roll.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audio (voice notes)
&lt;/h3&gt;

&lt;p&gt;Audio is sneaky because it’s &lt;strong&gt;time-based&lt;/strong&gt;. People talk longer than they type.&lt;/p&gt;

&lt;p&gt;A “quick note” becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transcription compute&lt;/li&gt;
&lt;li&gt;then LLM extraction&lt;/li&gt;
&lt;li&gt;then confirmation response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So one voice note can be more expensive than ten text messages—without looking scary in the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hidden cost #3: analytics turns “data” into “context inflation”
&lt;/h2&gt;

&lt;p&gt;The analytics requests were where I got hit hardest.&lt;/p&gt;

&lt;p&gt;Because analytics queries are vague by nature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“How am I doing this month?”&lt;/li&gt;
&lt;li&gt;“Why am I spending more than usual?”&lt;/li&gt;
&lt;li&gt;“What should I cut?”&lt;/li&gt;
&lt;li&gt;“Give me a weekly summary”&lt;/li&gt;
&lt;li&gt;“Predict next month”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To answer these, my first instinct was: &lt;strong&gt;send more context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I started retrieving lots of transactions, bundling them into the prompt, and asking the model to reason over it.&lt;/p&gt;

&lt;p&gt;That’s the trap.&lt;/p&gt;

&lt;p&gt;It &lt;em&gt;works&lt;/em&gt;… until it scales.&lt;/p&gt;

&lt;p&gt;Because now every analytics request is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a database read (sometimes large)&lt;/li&gt;
&lt;li&gt;plus prompt assembly&lt;/li&gt;
&lt;li&gt;plus a big generation (users love long insights)&lt;/li&gt;
&lt;li&gt;and it repeats weekly/monthly for every active user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where “AI features” quietly become “cloud bill features.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Hidden cost #4: latency causes retries, retries cause duplicates, duplicates cause chaos
&lt;/h2&gt;

&lt;p&gt;When the AI response took too long, users did what users do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send the same message again&lt;/li&gt;
&lt;li&gt;refresh&lt;/li&gt;
&lt;li&gt;tap the button twice&lt;/li&gt;
&lt;li&gt;re-upload the receipt&lt;/li&gt;
&lt;li&gt;re-record the voice note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if your system isn’t aggressively idempotent, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate transactions&lt;/li&gt;
&lt;li&gt;angry users (“why is lunch logged twice?”)&lt;/li&gt;
&lt;li&gt;extra model calls&lt;/li&gt;
&lt;li&gt;extra cleanup tools you now must build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the cost problem turned into a data integrity problem.&lt;/p&gt;

&lt;p&gt;And the data integrity problem turned back into a cost problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The builder takeaway: AI features are product architecture now
&lt;/h2&gt;

&lt;p&gt;Turning a messaging app into an expense tracker taught me something simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a chat UI, every UX decision is a cost decision.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auto-run analytics = cost spike&lt;/li&gt;
&lt;li&gt;long explanations by default = cost spike&lt;/li&gt;
&lt;li&gt;“just include more context” = cost spike&lt;/li&gt;
&lt;li&gt;retries without idempotency = cost spike &lt;em&gt;and&lt;/em&gt; bad data&lt;/li&gt;
&lt;li&gt;multimodal without guardrails = cost spike &lt;em&gt;and&lt;/em&gt; latency pain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you don’t need to fear it.&lt;/p&gt;

&lt;p&gt;You just need to design AI like you design infra:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;budgets&lt;/li&gt;
&lt;li&gt;fast paths&lt;/li&gt;
&lt;li&gt;async heavy work&lt;/li&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;graceful fallbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because once AI is inside your product, you’re not only building features anymore.&lt;/p&gt;

&lt;p&gt;You’re operating a meter.&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it’s helpful 📖. See you in the next article 🙌&lt;/p&gt;

</description>
      <category>ai</category>
      <category>telegram</category>
      <category>automation</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Web3 UX Finally Feels Normal in 2026: Smart Wallets, Account Abstraction, and the End of “Seed Phrase Panic”</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Thu, 29 Jan 2026 15:43:17 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/web3-ux-finally-feels-normal-in-2026-smart-wallets-account-abstraction-and-the-end-of-seed-2okf</link>
      <guid>https://dev.to/wildanzrrrr/web3-ux-finally-feels-normal-in-2026-smart-wallets-account-abstraction-and-the-end-of-seed-2okf</guid>
      <description>&lt;p&gt;A friend tried my Web3 app few years ago and bounced in under two minutes. Not because the product was bad (a bit maybe), but the ritual was bad. Install a wallet, save 12 or 24 words (actually he screenshot it, LOL). Learn what gas is, buy the right token just to pay fees. Sign a message that reads like legal boilerplate. Fail the first transaction anyway.&lt;/p&gt;

&lt;p&gt;Fast-forwards to 2026, and the same friend tried a different build of the same idea. They signed in with a passkey. The wallet was created in-app. The first transaction worked instantly. No “get ETH first” detour. If they lost their phone, recovery was possible without sacrificial offering.&lt;/p&gt;

&lt;p&gt;That’s the real emerging trend in 2026: not “new chains” or “new tokens”, but &lt;strong&gt;Web3 Onboarding and Daily Usage Getting Boring in a Good Way&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the reason is simple: &lt;strong&gt;account are becoming programmable.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was never “users don’t get crypto”
&lt;/h2&gt;

&lt;p&gt;It was that wallets were designed like raw infrastructure, not products. Most users still interact through EOAs (Externally Owned Accounts) which is a single private key that controls everythings. The EOA model is elegant for cryptography but terrible for human.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Seed phrase = single point of failure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every meaningful action needs a signature&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Users must hold the chain’s native token for fees&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No built-in recovery&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fine-grained permissions&lt;/strong&gt; (it’s basically “all access” or nothing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EOAs don’t just create friction; they create fear. One wrong click, one compromised device, one lost phrase—game over. That’s not a “UX issue.” That’s a product-killer.&lt;/p&gt;

&lt;p&gt;So in 2026, the most important shift is that more apps stop treating wallets like external luggage users must carry, and start treating them like &lt;strong&gt;part of the product surface&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Account abstraction (&lt;a href="https://docs.erc4337.io" rel="noopener noreferrer"&gt;ERC-4337&lt;/a&gt;) made wallets programmable without changing Ethereum consensus
&lt;/h2&gt;

&lt;p&gt;ERC-4337 is a pragmatic step toward “account abstraction” that works today using &lt;strong&gt;UserOperations&lt;/strong&gt;, an &lt;strong&gt;EntryPoint&lt;/strong&gt; contract, and &lt;strong&gt;bundlers&lt;/strong&gt; that include operations into blocks.&lt;/p&gt;

&lt;p&gt;You don’t need to memorize the whole architecture. The best mental model is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of “user sends a transaction,” think: &lt;strong&gt;user submits an intent&lt;/strong&gt;, and wallet logic decides how that intent gets executed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single idea unlocks the UX upgrades people have wanted since 2017 but couldn’t ship safely on EOAs.&lt;/p&gt;

&lt;h4&gt;
  
  
  1) Gas stops being the first boss fight
&lt;/h4&gt;

&lt;p&gt;With account abstraction, you can sponsor fees or let users pay fees in a different token (depending on chain/tooling and paymaster support). That removes the classic trap: “I need ETH to do anything.”&lt;/p&gt;

&lt;p&gt;In product terms: your onboarding doesn’t require a detour into an exchange.&lt;/p&gt;

&lt;h4&gt;
  
  
  2) Batching turns three scary popups into one normal flow
&lt;/h4&gt;

&lt;p&gt;Approve → swap → stake shouldn’t be three separate moments of anxiety. AA enables a wallet to batch multiple steps into a single operation (or at least a single user decision) so the user experiences “do the thing,” not “sign three times.”&lt;/p&gt;

&lt;h4&gt;
  
  
  3) Session keys kill the “sign every click” nightmare
&lt;/h4&gt;

&lt;p&gt;For games, social apps, and anything high-frequency, asking users to sign constantly is UX poison. Smart accounts can support scoped permissions (spend limits, allowed actions, time windows) so users can play without being interrupted every 30 seconds.&lt;/p&gt;

&lt;h4&gt;
  
  
  4) Recovery becomes an actual design space
&lt;/h4&gt;

&lt;p&gt;Because authorization logic lives in code, wallets can implement recovery patterns: multi-device, guardians, delays, spend limits, emergency locks. You’re no longer stuck with “hope your seed phrase is safe forever.”&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://eip7702.io" rel="noopener noreferrer"&gt;EIP-7702&lt;/a&gt; makes the migration story less painful
&lt;/h2&gt;

&lt;p&gt;Even if smart accounts are better, billions of dollars and years of behavior are tied to EOAs. That’s the awkward truth.&lt;/p&gt;

&lt;p&gt;EIP-7702 is part of the “make EOAs smarter” path, aiming to let EOAs opt into code-like behavior without forcing everyone to abandon their existing address and start over.&lt;/p&gt;

&lt;p&gt;For builders, the significance isn’t the exact opcode-level mechanics. It’s this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 is when “upgrade the wallet experience” stops sounding like “force users to migrate.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s a big reason UX improvements are finally showing up in mainstream apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedded wallets: the wallet install step starts disappearing
&lt;/h3&gt;

&lt;p&gt;Another 2026 shift: users increasingly don’t “go get a wallet” first. The wallet is embedded in the app.&lt;/p&gt;

&lt;p&gt;Embedded wallets aim to keep the benefits of self-custody (or at least non-custodial patterns) while removing the biggest drop-off point: browser extensions and seed phrase ceremonies during onboarding.&lt;/p&gt;

&lt;p&gt;This pairs perfectly with smart accounts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embed onboarding&lt;/li&gt;
&lt;li&gt;create a smart account automatically&lt;/li&gt;
&lt;li&gt;sponsor the first transactions&lt;/li&gt;
&lt;li&gt;reduce prompts with session keys&lt;/li&gt;
&lt;li&gt;let users “graduate” later (export, connect external wallet, add hardware key)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s the same playbook Web2 used for payments: start simple, add power later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passkeys: the missing bridge between “normal app login” and “secure wallet access”
&lt;/h3&gt;

&lt;p&gt;Passkeys are phishing-resistant, passwordless sign-in using cryptographic keys stored on devices (and often synced across devices securely).&lt;/p&gt;

&lt;p&gt;In 2026, passkeys matter because they let apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;offer familiar login UX&lt;/li&gt;
&lt;li&gt;reduce phishing compared to passwords&lt;/li&gt;
&lt;li&gt;integrate cleanly into recovery and multi-device flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your goal is adoption beyond crypto natives, passkeys are part of the “make it feel normal” stack—not because they’re trendy, but because they reduce the cognitive load users hate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a “normal” Web3 flow looks like in 2026
&lt;/h2&gt;

&lt;p&gt;Here’s the blueprint that keeps showing up in real products:&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding (under a minute)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;User signs in with passkey&lt;/li&gt;
&lt;li&gt;App creates an embedded wallet (or links one if they already have it)&lt;/li&gt;
&lt;li&gt;Smart account is initialized (AA-compatible)&lt;/li&gt;
&lt;li&gt;First action succeeds immediately (fees sponsored or abstracted)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Daily usage (no constant interruptions)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;routine actions use scoped session keys&lt;/li&gt;
&lt;li&gt;multi-step actions are batched&lt;/li&gt;
&lt;li&gt;user sees “confirm what you want,” not “sign unknown bytes”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recovery (designed, not improvised)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;multi-device recovery&lt;/li&gt;
&lt;li&gt;guardians / secondary verification&lt;/li&gt;
&lt;li&gt;optional time locks and spend limits&lt;/li&gt;
&lt;li&gt;emergency “freeze” if compromise is suspected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Web3 stops feeling like a hardware hobby and starts feeling like software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The builder’s decision: pick a wallet architecture like you’d pick an auth architecture
&lt;/h2&gt;

&lt;p&gt;If you’re writing for software devs, make this section crisp and opinionated.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option A: Smart accounts from day one (ERC-4337-first)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; consumer apps, games, social, anything you want to feel smooth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs:&lt;/strong&gt; you rely on bundler/paymaster infrastructure and must design safeguards carefully.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option B: Hybrid path (EOA today, upgrade path via 7702-style approach)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; products with existing users or existing addresses you don’t want to abandon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs:&lt;/strong&gt; complexity: two modes of account behavior, more edge cases.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option C: Embedded wallet + smart account (mainstream onboarding)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; retention and growth; it’s the lowest-friction path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs:&lt;/strong&gt; vendor/platform decisions, operational risk, and security responsibility.&lt;/p&gt;

&lt;p&gt;A useful way to frame it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wallet architecture is now product architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing: 2026 isn’t mass adoption—it’s mass retention
&lt;/h2&gt;

&lt;p&gt;Web3 spent years optimizing for getting users to show up.&lt;/p&gt;

&lt;p&gt;2026 is the pivot to keeping them.&lt;/p&gt;

&lt;p&gt;When onboarding feels normal, and failure states are survivable, developers stop spending half the roadmap on “wallet education” and start building actual product features. That’s why smart wallets and account abstraction are the trend worth writing about.&lt;/p&gt;

&lt;p&gt;The best sign you’re doing it right is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your user doesn’t think about the wallet. They think about the app.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it's helpful 📖. See you in the next article 🙌.&lt;/p&gt;

</description>
      <category>wallet</category>
      <category>crypto</category>
      <category>web3</category>
    </item>
    <item>
      <title>Deployment Workflow with GitHub Action 🚢</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Sat, 17 Jan 2026 05:22:44 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/deployment-workflow-with-github-action-14nm</link>
      <guid>https://dev.to/wildanzrrrr/deployment-workflow-with-github-action-14nm</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Deploying code can be a headache—pushing updates, running tests, and making sure everything works without breaking production. That's where deployment automation comes in. Instead of doing everything manually, you set up a system that handles the repetitive stuff for you.&lt;/p&gt;

&lt;p&gt;GitHub Actions makes this process way easier. Since it's built right into GitHub, you don't need to juggle multiple tools or services. You can set up automated workflows that test your code, build your project, and deploy it—all triggered by things like pushing commits or merging pull requests. It's basically your personal deployment assistant that works whenever you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is GitHub Actions?
&lt;/h2&gt;

&lt;p&gt;GitHub Actions is built around a few simple ideas that work together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows&lt;/strong&gt; are like instruction manuals that tell GitHub what to do. They're just text files that sit in your project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; are the major tasks in your workflow—like "test the code" or "deploy to production." They can run one after another or all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt; are the individual actions within each job. Think of them as the actual commands you'd type if you were doing this manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runners&lt;/strong&gt; are the computers that actually do the work. GitHub provides them for free, or you can use your own if you need something specific.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why people like using GitHub Actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's already part of GitHub, so you don't need to sign up for another service or sync anything.&lt;/li&gt;
&lt;li&gt;You can automate pretty much anything—testing, building, deploying, even sending notifications.&lt;/li&gt;
&lt;li&gt;It's flexible enough to work with almost any language, framework, or deployment target you can think of.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up Your First Deployment
&lt;/h2&gt;

&lt;p&gt;Okay, enough of the lecture, let’s start the implementation right away. For this tutorial, we’ll do a simple deployment on a Nest.js application. To help you better understand the flow, let’s take a look at this picture flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6x4hfqv4kqk3mczo8xig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6x4hfqv4kqk3mczo8xig.png" alt="Our workflow example" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you push to the &lt;code&gt;staging&lt;/code&gt; branch, it triggers the workflow. There are two jobs: &lt;strong&gt;build-and-publish&lt;/strong&gt; and &lt;strong&gt;deploy&lt;/strong&gt;. The first job builds the project and uploads the package to the cloud container registry. The second job retrieves environment variables from GitHub secrets, securely sends them to the VPS, pulls the update, and runs the latest package version. That’s it. Pretty simple, right?&lt;/p&gt;

&lt;p&gt;Alright, now let’s make our hands dirty. First, you need to create a folder like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;workflows&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;feel&lt;/span&gt; &lt;span class="nx"&gt;free&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;rename&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All GitHub Actions files go inside the &lt;code&gt;.github/workflows&lt;/code&gt; folder. &lt;/p&gt;

&lt;p&gt;Now, let's define the first rule to determine when the workflow should run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Staging&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Push&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;Deploy&lt;/span&gt;

&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ghcr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;
  &lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this step, we want the workflow to be triggered when someone pushes code to the staging branch. We'll also define environment variables that will be used in the next steps.&lt;/p&gt;

&lt;p&gt;Then, we’ll define our first job, which is &lt;strong&gt;Build and Push&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;
    &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;
      &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;write&lt;/span&gt;
    &lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;image_tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get_sha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;short_sha&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="nl"&gt;repo_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lowercase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

    &lt;span class="nl"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Checkout&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v4&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt; &lt;span class="nx"&gt;commit&lt;/span&gt; &lt;span class="nx"&gt;SHA&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;get_sha&lt;/span&gt;
        &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short_sha=$(git rev-parse --short HEAD)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full_sha=$(git rev-parse HEAD)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="nx"&gt;Docker&lt;/span&gt; &lt;span class="nx"&gt;Buildx&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;buildx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v3&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Login&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;GitHub&lt;/span&gt; &lt;span class="nx"&gt;Container&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v3&lt;/span&gt;
        &lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REGISTRY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lowercase&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lowercase&lt;/span&gt;
        &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Extract&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;Docker&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v5&lt;/span&gt;
        &lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REGISTRY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="sr"&gt;/${{ steps.lowercase.outputs.owner }}/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;short&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;staging&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Build&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="nx"&gt;Docker&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v5&lt;/span&gt;
        &lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;
          &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;Dockerfile&lt;/span&gt;
          &lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;gha&lt;/span&gt;
          &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;gha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;
          &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="nx"&gt;COMMIT_SHA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get_sha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;short_sha&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nl"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set permissions to read content and write packages. This lets us read the repository content (source code) and publish to GitHub Container Registry. We also create outputs for &lt;code&gt;image_tag&lt;/code&gt; and &lt;code&gt;repo_owner&lt;/code&gt;. &lt;br&gt;
First, we check out the current branch and get the short and full SHA commit. Then we set up Docker Buildx (basically a Docker builder) and log in to GitHub Container Registry. Next, we transform the repository owner to lowercase (ghcr forces package names to be lowercase :D). Finally, we grab the metadata, build the image, and push the package to the registry.&lt;/p&gt;

&lt;p&gt;Great, now let’s define our second job, which is &lt;strong&gt;deploy.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;
    &lt;span class="nx"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;
    &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;
    &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Checkout&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v4&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Create&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;
        &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;SECRETS_JSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nf"&gt;toJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Parse&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;
          &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$SECRETS_JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/tmp/&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;

          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Read&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;
          &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nx"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Skip&lt;/span&gt; &lt;span class="nx"&gt;comments&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=~&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;[[:&lt;/span&gt;&lt;span class="nx"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${line// }&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;]];&lt;/span&gt; &lt;span class="nx"&gt;then&lt;/span&gt;
              &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="nx"&gt;fi&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Extract&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="nf"&gt;name &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=~&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Z_&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Z0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;]];&lt;/span&gt; &lt;span class="nx"&gt;then&lt;/span&gt;
              &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${BASH_REMATCH[1]}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

              &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;
              &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jq&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.[$key] // empty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

              &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${key}=${value}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
            &lt;span class="nx"&gt;fi&lt;/span&gt;
          &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;

          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Clean&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
          &lt;span class="nx"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;

          &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generated .env file:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;cat&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Copy&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appleboy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;scp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;1.7&lt;/span&gt;
        &lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_HOST&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_USER&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
          &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="nx"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SSH&lt;/span&gt; &lt;span class="nx"&gt;Deploy&lt;/span&gt;
        &lt;span class="nx"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appleboy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;1.6&lt;/span&gt;
        &lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_HOST&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_USER&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VPS_STAGING_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="nl"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt; &lt;span class="nx"&gt;deployment&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt;
            &lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Restore&lt;/span&gt; &lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;
            &lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="nx"&gt;restore&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Pull&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;
            &lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="nx"&gt;pull&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Login&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;GitHub&lt;/span&gt; &lt;span class="nx"&gt;Container&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;
            &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="nx"&gt;ghcr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${{ github.actor }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;
            &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image_tag&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo_owner&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;BUILT_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;ghcr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/${{ env.PROJECT_KEY }}:${IMAGE_TAG&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;
            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Pull&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;
            &lt;span class="nx"&gt;docker&lt;/span&gt; &lt;span class="nx"&gt;pull&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BUILT_IMAGE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Replace&lt;/span&gt; &lt;span class="nx"&gt;placeholders&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yml&lt;/span&gt;
            &lt;span class="nx"&gt;sed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s|--built-image--|${BUILT_IMAGE}|g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yml&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Stop&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="nx"&gt;containers&lt;/span&gt;
            &lt;span class="nx"&gt;docker&lt;/span&gt; &lt;span class="nx"&gt;compose&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yml&lt;/span&gt; &lt;span class="nx"&gt;down&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Sleep&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;few&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;ensure&lt;/span&gt; &lt;span class="nx"&gt;proper&lt;/span&gt; &lt;span class="nx"&gt;shutdown&lt;/span&gt;
            &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Start&lt;/span&gt; &lt;span class="nx"&gt;services&lt;/span&gt;
            &lt;span class="nx"&gt;docker&lt;/span&gt; &lt;span class="nx"&gt;compose&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yml&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This job depends on the &lt;code&gt;build-and-push&lt;/code&gt; job, so it waits until that finishes. We specify the GitHub environment as &lt;code&gt;staging&lt;/code&gt; (more on this below). &lt;br&gt;
First, we check out the current branch. Then we create an .env file based on .env.example and inject values from GitHub environment variables. Next, we securely send the .env file via SSH to our project directory. Finally, we pull the latest updates, retrieve the newest package, and run the application.&lt;/p&gt;

&lt;p&gt;If you are interested in seeing the full implementation code, feel free to check &lt;a href="https://github.com/wildanzrrr/github-action-example/tree/staging" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nice work! Now let's check out GitHub Actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkbon97sbidsy5bnmlt2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkbon97sbidsy5bnmlt2.png" alt="GitHub Action Tab" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have two actions triggered—one failed and one successful. Click on either action to see more details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa27corgk5pkkvt0f27oc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa27corgk5pkkvt0f27oc.png" alt="GitHub Action Details" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you can inspect your workflow and see what's happening. Just expand each step to see more details about the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So yeah, GitHub Actions is pretty solid for automating deployments. Once you've got your workflows set up, you don't have to manually push updates or worry about whether you forgot a step. It just handles the build, test, and deploy cycle for you. Whether you're working solo on a side project or with a team on something bigger, automating this stuff frees you up to actually build features instead of babysitting deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Setting up GitHub Environment
&lt;/h2&gt;

&lt;p&gt;In our workflow, we're using GitHub Environment to store sensitive secrets and environment variables. Whenever you need to update or add new secrets, you can do it directly in GitHub. Just go to your repository settings and click on &lt;strong&gt;Environments&lt;/strong&gt;. From there, you can create a new environment with whatever name you want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fommkw14rcyktxnojb8k4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fommkw14rcyktxnojb8k4.png" alt="Creating Environment" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, add a deployment rule to specify which branch can use this environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lk2e0iogxn63zu3k6t7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lk2e0iogxn63zu3k6t7.png" alt="Setting Rules" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can just add your environment secrets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvks6acctg5hp2uy1sz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvks6acctg5hp2uy1sz6.png" alt="Add or Edit Secrets" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it's helpful 📖. Don't forget to follow the account for the latest updates 🌟. See you in the next article 🙌.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>automation</category>
      <category>github</category>
    </item>
    <item>
      <title>All you need to know and to get started building your first MCP 🤖</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Sun, 11 Jan 2026 03:10:10 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/all-you-need-to-know-and-to-get-started-building-your-first-mcp-5766</link>
      <guid>https://dev.to/wildanzrrrr/all-you-need-to-know-and-to-get-started-building-your-first-mcp-5766</guid>
      <description>&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Definition and basic overview&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Model Context Protocol (MCP) is a standard method for connecting AI applications with external systems. It can be a database, function, tool (e.g., search the web, create PDFs), or any process that helps AI better understand context and perform tasks. If you haven't realized it, most AI products already have this integration. A great example: when you ask something in &lt;strong&gt;ChatGPT&lt;/strong&gt; or &lt;strong&gt;Grok&lt;/strong&gt;, sometimes you'll see a background process called "search the web." That's MCP behind the scenes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why you should learn it&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Today, anyone can create a small app to speed up daily tasks with AI. However, results often fall short because the app lacks full context about the question. What if you could give the AI complete information from your database? That's where MCP comes in. It helps AI better understand context before generating responses. The AI can execute available tools in the MCP and use the results to produce more accurate, context-aware outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture of MCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Key participants&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There are 3 key participants of MCP, which are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Host&lt;/strong&gt;: The AI application that hosts, manages, and coordinates one or multiple MCP servers. It is responsible for deciding &lt;em&gt;when&lt;/em&gt; to request external context and &lt;em&gt;which&lt;/em&gt; tools should be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;: The service that exposes tools and context. It defines available capabilities, executes requests, and returns structured results back to the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Client&lt;/strong&gt;: The component that maintains the connection to an MCP server. It handles communication, sends requests, receives responses, and forwards context to the MCP host.&lt;strong&gt;Key participants&lt;/strong&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Building Blocks for MCP&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Beyond the key participants, MCP relies on several core building blocks. These components work together to make tool execution reliable, secure, and context-aware. Not every MCP server needs all of them, but production-ready systems usually implement most of these blocks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tool Definitions
&lt;/h4&gt;

&lt;p&gt;Tool definitions describe &lt;strong&gt;what the MCP server can do&lt;/strong&gt;. Each tool clearly specifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The tool name&lt;/li&gt;
&lt;li&gt;What the tool does&lt;/li&gt;
&lt;li&gt;Required input parameters&lt;/li&gt;
&lt;li&gt;The expected output structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These definitions allow the AI to reason about &lt;em&gt;which tool to call&lt;/em&gt;, &lt;em&gt;when to call it&lt;/em&gt;, and &lt;em&gt;how to use the result&lt;/em&gt;. Well-designed tool definitions are critical—ambiguous or overly complex tools often lead to poor AI decisions.&lt;/p&gt;

&lt;p&gt;Good tool definitions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit and predictable&lt;/li&gt;
&lt;li&gt;Narrow in responsibility&lt;/li&gt;
&lt;li&gt;Deterministic in output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clearer the definition, the better the AI can use it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Transport Layer
&lt;/h4&gt;

&lt;p&gt;The transport layer defines &lt;strong&gt;how the MCP client and server communicate&lt;/strong&gt;. It handles message delivery, connection lifecycle, and data streaming.&lt;/p&gt;

&lt;h5&gt;
  
  
  &lt;strong&gt;1. stdio (Standard Input/Output)&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;This method uses standard input and output streams for communication. The MCP server runs as a child process and communicates via stdin/stdout.&lt;/p&gt;

&lt;p&gt;Best suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local development&lt;/li&gt;
&lt;li&gt;CLI-based tools&lt;/li&gt;
&lt;li&gt;Single-user environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s simple, fast, and easy to debug, making it ideal for early-stage MCP development.&lt;/p&gt;

&lt;h5&gt;
  
  
  &lt;strong&gt;2. SSE (Server-Sent Events)&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;SSE enables one-way, real-time communication from server to client over HTTP. The server can push updates whenever new data is available.&lt;/p&gt;

&lt;p&gt;Best suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remote MCP servers&lt;/li&gt;
&lt;li&gt;Web-based clients&lt;/li&gt;
&lt;li&gt;Long-running or streaming tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because SSE is built on standard web protocols, it works well with existing infrastructure and firewalls.&lt;/p&gt;

&lt;h4&gt;
  
  
  Context Injection
&lt;/h4&gt;

&lt;p&gt;Context injection provides &lt;strong&gt;relevant background information&lt;/strong&gt; to the AI &lt;em&gt;before&lt;/em&gt; it generates a response. This may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User profile data&lt;/li&gt;
&lt;li&gt;Recent conversation history&lt;/li&gt;
&lt;li&gt;Database records&lt;/li&gt;
&lt;li&gt;Application state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of forcing the AI to guess, MCP allows you to explicitly supply the information it needs. Strong context injection dramatically improves accuracy, relevance, and consistency.&lt;/p&gt;

&lt;p&gt;Designing good context is often more important than model choice. Poor context leads to hallucinations; good context leads to reliable outputs. You can learn more on how to build better context injection at &lt;a href="https://context7.com/" rel="noopener noreferrer"&gt;https://context7.com/&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Security &amp;amp; Permissions
&lt;/h4&gt;

&lt;p&gt;Security controls &lt;strong&gt;who can access which tools and data&lt;/strong&gt;. This is essential when MCP is connected to sensitive systems such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User databases&lt;/li&gt;
&lt;li&gt;Financial data&lt;/li&gt;
&lt;li&gt;Internal APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common security mechanisms include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication (API keys, tokens, OAuth)&lt;/li&gt;
&lt;li&gt;Authorization (role-based or permission-based access)&lt;/li&gt;
&lt;li&gt;Tool-level restrictions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A best practice is to treat MCP tools as &lt;strong&gt;privileged operations&lt;/strong&gt; and only expose what is strictly necessary.&lt;/p&gt;

&lt;h4&gt;
  
  
  Observability &amp;amp; Error Handling
&lt;/h4&gt;

&lt;p&gt;Observability helps you understand &lt;strong&gt;what the MCP system is doing at runtime&lt;/strong&gt;. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool invocation logs&lt;/li&gt;
&lt;li&gt;Execution latency&lt;/li&gt;
&lt;li&gt;Success and failure rates&lt;/li&gt;
&lt;li&gt;Error messages and stack traces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Proper error handling ensures that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failures are surfaced clearly to the host&lt;/li&gt;
&lt;li&gt;Partial failures don’t crash the system&lt;/li&gt;
&lt;li&gt;The AI can recover or retry when possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without observability, MCP systems become difficult to debug and unsafe to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helpful Resources
&lt;/h2&gt;

&lt;p&gt;If you want to go deeper and start building with MCP, the following resources are a great place to begin:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Getting Started Guide&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Learn the fundamentals of MCP, its design philosophy, and how to set up your first MCP server.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/getting-started/intro&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Official Examples&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Explore real-world MCP implementations and see how tools, transports, and context are put together.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://modelcontextprotocol.io/examples" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/examples&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Community Discussions &amp;amp; Support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask questions, share ideas, and learn from others building with MCP.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/orgs/modelcontextprotocol/discussions" rel="noopener noreferrer"&gt;https://github.com/orgs/modelcontextprotocol/discussions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Secure protected API with HMAC! Next Level of API Keys 🔐</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Sun, 31 Mar 2024 09:42:24 +0000</pubDate>
      <link>https://dev.to/wildanzrrrr/secure-protected-api-with-hmac-next-level-of-api-keys-4l39</link>
      <guid>https://dev.to/wildanzrrrr/secure-protected-api-with-hmac-next-level-of-api-keys-4l39</guid>
      <description>&lt;p&gt;Is using API Keys enough to protect private APIs? Certainly not. The way API Keys work is by checking and validating whether the request sent by the client contains an API key, which is usually embedded in the header or query request. If the API key exists and is valid, the request is allowed, otherwise not.&lt;/p&gt;

&lt;p&gt;This simple method may sound secure because only someone with a valid API key can access it. However, there's a security aspect that allows someone to steal the API key. The API Keys method embeds the API key directly into the header or query of the request, allowing others to see the content inserted into that request. As a result, someone can intercept the request and steal the API key 😮.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xre48mqzvtblogdp01p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xre48mqzvtblogdp01p.png" alt="API Key exposed, source: Google Images" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are many API authentication methods that can be used to protect private APIs, one of the most popular being *&lt;em&gt;JWT *&lt;/em&gt;(JSON Web Token). This method requires the client to log in to the system and obtain a token to access the API. However, this method is sometimes not ideal for APIs that involve data sharing or matrix reports because the client must log in first and secure their token to access the API. It should be noted that JWT tokens are embedded in the header, which can also be intercepted and stolen by others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is HMAC?&lt;/strong&gt;&lt;br&gt;
Another authentication method to consider is *&lt;em&gt;HMAC *&lt;/em&gt;(Hash-based Message Authentication Code). This method uses a Public key embedded in the header or query and a Secret key to calculate the hash of a request data. It is important to note that the secret key will not be embedded in the request but will be used for hashing the request data. The resulting hash (usually called the signature) is then embedded along with the public key. By implementing HMAC, the server will validate the request by comparing the hash value. If the hash values match, the request will be allowed, otherwise not.&lt;/p&gt;

&lt;p&gt;The HMAC method depends on the similarity of the secret key and the hashing algorithm used. For example, using "this-is-super-secret-key" as the secret key and HmacSHA256 as the chosen hashing algorithm. Then the server and client must use the same secret key and hashing algorithm to verify that the request is authentic. With this technique, someone attempting to intervene in the request will not be able to use the same signature for another request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros and Cons&lt;/strong&gt;&lt;br&gt;
Although using HMAC seems secure enough, there are still advantages and disadvantages to this method. It would be better if this method is combined with other security measures such as IP Locking, Rate Limiting, and so on. The advantages offered by this method are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data Integrity: ensuring secure communication by verifying the integrity of the request.&lt;/li&gt;
&lt;li&gt;Protection Against Unauthorized Access: the API can only be accessed by clients with a valid secret key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The disadvantages of this method are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key management and social engineering: ensuring that the secret key is not leaked into the wrong hands.&lt;/li&gt;
&lt;li&gt;Performance overhead: verifying the signature by a server receiving too many requests will require higher computational resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Auth Flow&lt;/strong&gt;&lt;br&gt;
So what is the actual process of the HMAC method, is it simple or quite complicated? Let's look at the following diagrams to get an idea.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbm9gjxzwrknkso4gng4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbm9gjxzwrknkso4gng4.png" alt="Process of generating a Hash-based Message Authentication Code (HMAC)" width="800" height="350"&gt;&lt;/a&gt;&lt;br&gt;
It looks easy, right? The request data is combined with the secret key to create the digest algorithm and then the HMAC is calculated. Okay, now take a look at the following diagram to see how the process goes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnbjmlb7aehn11xy9ymb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnbjmlb7aehn11xy9ymb.png" alt="Auth flow with HMAC between client and server" width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both the server and client agree on the public key, secret key, and algorithm to be used.&lt;/li&gt;
&lt;li&gt;The client will create a signature based on the requested data with the secret key.&lt;/li&gt;
&lt;li&gt;The client inserts the public key and signature into the header request.&lt;/li&gt;
&lt;li&gt;The server receives the request and verifies it by replicating the signature creation with the data (taken from the header and body request) and the same secret key.&lt;/li&gt;
&lt;li&gt;If the signature matches, the request will be allowed, and vice versa, if not, it will be returned with an error 401 (Unauthorized).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How to code?&lt;/strong&gt;&lt;br&gt;
Okay, now it's time for practice! Here is a simple pseudocode to implement the HMAC method. If you're interested in seeing actual program code using the Nest.js framework, please check the &lt;a href="https://github.com/Wildanzr/articles_hmac_auth" rel="noopener noreferrer"&gt;following GitHub repository&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUBLIC_KEY = req.headers[x-api-key];
SIGNATURE_KEY = req.headers[x-signature-key]
REQUEST_DATA = req.body;

// Check if public key and signature exist
if (!PUBLIC_KEY &amp;amp;&amp;amp; !SIGNATURE_KEY) {
  throw Error(401)
}

// Regenerate Signature
SERVER_SIGNATURE = generateSignature(PUBLIC_KEY, SIGNATURE_KEY, REQUEST_DATA)

// Verify Signature
if (SERVER_SIGNATURE == SIGNATURE_KEY) {
  return OK(200)
} else {
  return Error(401)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Hmm, so are you interested in implementing the next level of API Keys? By using the HMAC method, API security will be maintained with cryptographic principles that are impossible to reverse engineer (perhaps if Quantum Computing is operational, cryptographic algorithms will be different 😃).&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it's helpful 📖. Don't forget to follow the account for the latest updates 🌟. See you in the next article 🙌.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>security</category>
      <category>backend</category>
    </item>
    <item>
      <title>Node.js Send Email using Google OAuth2</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Tue, 25 Apr 2023 08:33:06 +0000</pubDate>
      <link>https://dev.to/wildanzr/nodejs-send-email-using-google-oauth2-49l5</link>
      <guid>https://dev.to/wildanzr/nodejs-send-email-using-google-oauth2-49l5</guid>
      <description>&lt;p&gt;One common requirement of back-end application development is sending a message via email. This feature can support business processes such as verifying user data, notifications, account security, marketing, and many other use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then, how can we send an email message in a Node.js application?
&lt;/h3&gt;

&lt;p&gt;One of the libraries that are widely used in sending emails is &lt;a href="https://nodemailer.com/about/" rel="noopener noreferrer"&gt;&lt;strong&gt;Nodemailer&lt;/strong&gt;&lt;/a&gt;. There is the easiest way of implementation which using the &lt;a href="https://support.google.com/accounts/answer/185833?hl=id" rel="noopener noreferrer"&gt;&lt;strong&gt;Google App Password&lt;/strong&gt;&lt;/a&gt; service. However, this method has been labelled by Google as &lt;strong&gt;less secure&lt;/strong&gt; because the result of generated password is easy to memorize and less complex, so the security level is lower.&lt;/p&gt;

&lt;p&gt;Alternatively, there is a way in sending an email using the  &lt;a href="https://developers.google.com/gmail/imap/xoauth2-protocol?hl=id" rel="noopener noreferrer"&gt;&lt;strong&gt;Google OAuth2&lt;/strong&gt;&lt;/a&gt; method. &lt;em&gt;OAuth2&lt;/em&gt; (Open Authorization 2.0) is an authentication and authorization protocol used to allow third parties to access protected resources. This method has a high level of security and has been used as a security standard in the technology industry.&lt;/p&gt;

&lt;p&gt;Ok, shall we begin :)&lt;/p&gt;

&lt;p&gt;The first thing to do is to create a project in the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Developer Console&lt;/strong&gt;&lt;/a&gt; (make sure you have a Google account) :D&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc5s2c8auiodq23nn75m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc5s2c8auiodq23nn75m.png" alt="Create project in google developer console (1)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif6mnximjlyzc4bh0pcy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif6mnximjlyzc4bh0pcy.png" alt="Create project in google developer console (2)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the project is successfully created, go to the left tab in the section &lt;strong&gt;API &amp;amp; Services &amp;gt; OAuth Consent Screen&lt;/strong&gt; then select &lt;strong&gt;External&lt;/strong&gt; configuration. Next, fill in the app name data, user support email, and developer contact information, then follow the next instructions and save. To activate the OAuth Consent Screen configuration, do &lt;strong&gt;Publish App&lt;/strong&gt; and confirm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzk0mvwc45qiu9cnour1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzk0mvwc45qiu9cnour1.png" alt="OAuth consent screen configuration (1)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bbks91b15e85zgr400y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bbks91b15e85zgr400y.png" alt="OAuth consent screen configuration (2)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great, it's time to configure the OAuth Client ID. On the tab &lt;strong&gt;API &amp;amp; Service&lt;/strong&gt;, select the menu &lt;strong&gt;Credential &amp;gt; Create Credentials &amp;gt; OAuth Client ID&lt;/strong&gt;. After that, fill the application type with a &lt;strong&gt;web application&lt;/strong&gt;, a name with the application name (feel free to naming), and add the URI &lt;strong&gt;&lt;a href="https://developers.google.com/oauthplayground" rel="noopener noreferrer"&gt;https://developers.google.com/oauthplayground&lt;/a&gt;&lt;/strong&gt; in the &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt; section. Creating an OAuth Client ID generates credentials in the form of an ID and a secret (keep it safe, you can download it in JSON format).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fty3laycsiwvu7kcl4n45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fty3laycsiwvu7kcl4n45.png" alt="OAuth Client ID configuration (1)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6b5qn9e2no39s6fd2zeb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6b5qn9e2no39s6fd2zeb.png" alt="OAuth Client ID configuration (2)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3myreyklmr5xk3ystnwt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3myreyklmr5xk3ystnwt.png" alt="OAuth Client ID configuration (3)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nice, the next step is to test OAuth Client ID and get the refresh token. Visit &lt;a href="https://developers.google.com/oauthplayground" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Developer OAuth Playground&lt;/strong&gt;&lt;/a&gt;, set OAuth Client ID and secret configuration using your credentials, then select &lt;a href="http://mail.google.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;http://mail.google.com/&lt;/strong&gt;&lt;/a&gt;. Next, you will be redirected to the Google Authentication pages, choose the same account used to create OAuth Client ID. If you are facing a page that is not safe, open the menu &lt;strong&gt;Advanced Options&lt;/strong&gt; and click &lt;strong&gt;Continue (not safe)&lt;/strong&gt; (this is happen because we are not submitting the verification app to Google, but you can ignore it for a moment). The result is an &lt;strong&gt;authentication code, refresh token, and access token&lt;/strong&gt; (once more, keep it safe).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazk7z39r1n67o44t3dil.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazk7z39r1n67o44t3dil.png" alt="OAuth test and getting refresh token (1)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft67m9g8wfdqkzw6shj8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft67m9g8wfdqkzw6shj8v.png" alt="OAuth test and getting refresh token (2)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqfxyc4440sz0zqc0jrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqfxyc4440sz0zqc0jrg.png" alt="OAuth test and getting refresh token (3)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow, the configuration is quite long, whereas we are not yet writing a single piece of code. But now, let's write some code. Start with creating an empty folder then initialize the project (fill it according to the project initialization instructions).&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

npm init


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We will code using Typescript, if you are more prefer using Javascript just install the 3 dependencies needed (however you will need some adjustments from Typescript to Javascript).&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

npm i --save-dev typescript ts-node @types/node


&lt;/code&gt;&lt;/pre&gt;

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

npm i dotenv nodemailer googleapis


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Great, now create a file with an &lt;strong&gt;.env&lt;/strong&gt; extension to store the credentials locally.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

OAUTH_EMAIL=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_REFRESH_TOKEN=


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Nice, now create an &lt;strong&gt;index.ts&lt;/strong&gt; file. The first thing to do is import the required dependencies and get the value of our environment variables.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Next, create an OAuth2 Client object and make a getAccessToken request. This function returns a value in the form of an accessToken code which is used as the authorization when sending emails.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Lastly, create a transporter configuration with SMTP and set the mailOptions as the email sending object.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Congratulations! Email sending using the OAuth2 method was successful. With the steps that have been followed, the email sent will be guaranteed to be secure and properly authenticated. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using the OAuth2 method to send email via Nodemailer has several advantages, including: increasing security by avoiding using usernames and passwords, enabling good integration with third-party email services and can increase application scalability.&lt;/p&gt;

&lt;p&gt;If you want to get this source code, feel free to take a look at &lt;a href="https://github.com/Wildanzr/art_nodejs_gmail_oauth" rel="noopener noreferrer"&gt;this GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it's useful 📖. Don't forget to follow the account to get the latest information🌟. See you in the next article🙌.&lt;/p&gt;

</description>
      <category>oauth2</category>
      <category>gmail</category>
      <category>nodemai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Express file upload using Multer</title>
      <dc:creator>Graita Sukma Febriansyah Triwildan Azmi</dc:creator>
      <pubDate>Mon, 03 Apr 2023 07:34:45 +0000</pubDate>
      <link>https://dev.to/wildanzr/express-file-upload-using-multer-1a47</link>
      <guid>https://dev.to/wildanzr/express-file-upload-using-multer-1a47</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wpfdeda4yqp76o1eo4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wpfdeda4yqp76o1eo4b.png" alt="API with upload "&gt;&lt;/a&gt;&lt;br&gt;
When you are developing an &lt;strong&gt;API&lt;/strong&gt; (Application Programming Interface) there must be a requirement which allows users to be able to upload or download file. A simple example of this implementation is the user able to change his picture profile. In this case, data sent by the user is not &lt;strong&gt;JSON&lt;/strong&gt; (Javascript Object Notation), but the file data type. To send this data, the user has to send through the body using multipart/form data format.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the library to be used to create an API which able to upload or download files? &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer to that question is Multer. &lt;a href="https://www.npmjs.com/package/multer" rel="noopener noreferrer"&gt;&lt;strong&gt;Multer&lt;/strong&gt;&lt;/a&gt; is one of the node.js libraries in the form of middleware which able to handle files through &lt;strong&gt;multipart/form data&lt;/strong&gt;. You need to know that multipart/form data is one of content-type inside HTML form.&lt;/p&gt;

&lt;p&gt;Multer works with adding an object body and object file to the request body. Through this, there is data containing a value from the text field and file data which is attached in HTML form. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How to create an API which able to upload and download the file in node.js?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To answer this question, we will build a simple express app which has two endpoints for uploading and downloading a file. The first step is to install the multer dependencies.&lt;br&gt;
&lt;code&gt;npm i express multer&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ok, before we start to write the code, we will create a folder structure like this:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

src/
├── upload/
└── index.js


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Great, next we will import the required dependencies. &lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Nice, now we will create multer configuration, upload function, and delete file function.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;You can see in the configuration section, we setting &lt;strong&gt;uploads&lt;/strong&gt; folder as a file storage directory. Then we set file naming so that there are no similarities in names. On the &lt;strong&gt;upload&lt;/strong&gt; function, we set &lt;strong&gt;diskStorage&lt;/strong&gt; as storage and apply a file limit size of 25Mb. On the &lt;strong&gt;deleteFile&lt;/strong&gt; function, we create a function which accepts one argument as the file we want to delete.&lt;/p&gt;

&lt;p&gt;Hmm... looks like something is missing. Validation? Yup, this part is responsible to ensure the file format under the data we want.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Excellent, now is the time for us to implement the configurations and functions that have been made in the route API.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Great job, now we have two routes which are &lt;strong&gt;uploads&lt;/strong&gt; with &lt;strong&gt;HTTP method POST&lt;/strong&gt; and &lt;strong&gt;uploads/:id&lt;/strong&gt; with &lt;strong&gt;HTTP method GET&lt;/strong&gt;. On the route &lt;strong&gt;POST&lt;/strong&gt;, we applied a function &lt;strong&gt;upload.single&lt;/strong&gt; with an argument &lt;em&gt;'file'&lt;/em&gt;. This &lt;em&gt;'file'&lt;/em&gt; is the body of the form data use to send the file. You can change the name of &lt;em&gt;'file'&lt;/em&gt; with others, such as &lt;em&gt;'picture'&lt;/em&gt;. Next, a check is applied to ensure the file is not empty and valid with our format. On the route &lt;strong&gt;GET&lt;/strong&gt;, we use params &lt;strong&gt;:id&lt;/strong&gt; to get the file that we want. Next, it will check whether the file exist or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;To create an API which able to receive a file, we can use &lt;a href="https://www.npmjs.com/package/multer" rel="noopener noreferrer"&gt;&lt;strong&gt;Multer&lt;/strong&gt;&lt;/a&gt;. On the route we have created, it applied a function &lt;strong&gt;upload.single&lt;/strong&gt; to accept one file. If you want to accept more than one file, you can use the function &lt;strong&gt;upload.array&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to get this source code, feel free to take a look at &lt;a href="https://github.com/Wildanzr/art_nodejs_upload_file" rel="noopener noreferrer"&gt;this GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading this article, hope it's useful 📖. Don't forget to follow the account to get the latest information🌟. See you in the next article🙌.&lt;/p&gt;

</description>
      <category>express</category>
      <category>node</category>
      <category>upload</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
