<?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: RainyTech</title>
    <description>The latest articles on DEV Community by RainyTech (@rainytechlab).</description>
    <link>https://dev.to/rainytechlab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4162935%2F27cc550f-5194-4e90-9458-ca7eca4afa39.jpg</url>
      <title>DEV Community: RainyTech</title>
      <link>https://dev.to/rainytechlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rainytechlab"/>
    <language>en</language>
    <item>
      <title>PHP Got Hacked? PHP2Shell, LiteLLM, GitLab and a CA Key of 4: What Devs Should Update Now</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Sun, 25 Oct 2026 02:04:26 +0000</pubDate>
      <link>https://dev.to/rainytechlab/php-got-hacked-php2shell-litellm-gitlab-and-a-ca-key-of-4-what-devs-should-update-now-5470</link>
      <guid>https://dev.to/rainytechlab/php-got-hacked-php2shell-litellm-gitlab-and-a-ca-key-of-4-what-devs-should-update-now-5470</guid>
      <description>&lt;p&gt;&lt;em&gt;AI-assisted disclosure: I used an AI assistant to help structure this write-up. Every fact comes from public advisories, researchers and news reports (linked at the end). Nothing here was hacked or tested by me against anyone's systems.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version: update PHP to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 and restart it, check every Python environment for a file called &lt;code&gt;cook.pth&lt;/code&gt;, upgrade self-managed GitLab (even if you patched on October 12), and keep your OS, browser, Go, pip and certifi up to date.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;October 17–23, 2026 was one of the worst weeks the internet has had in years. The NSA told everyone running PHP to get ready for an emergency update. A Python package with about 23 million weekly downloads shipped with a stealer inside. GitLab patched a maximum-severity bug that had been hiding since 2014. Banks found AI agents wandering around inside customer accounts. And a company trusted to protect HTTPS used a secret key that was... the number 4.&lt;/p&gt;

&lt;p&gt;📺 Full video (8 min): &lt;a href="https://youtu.be/KNXtuCbvxDw" rel="noopener noreferrer"&gt;https://youtu.be/KNXtuCbvxDw&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. PHP2Shell: a 9.2 bug in PHP itself
&lt;/h2&gt;

&lt;p&gt;PHP runs roughly 70% of websites, including WordPress, Drupal and Joomla. PHP2Shell is a pair of flaws in PHP 5.4 through 8.5:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-109618&lt;/strong&gt;: out-of-memory corruption that leads to unauthenticated code execution as the PHP web worker. Researchers demonstrated pre-auth RCE on WordPress, Drupal and Joomla.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-109619&lt;/strong&gt;: stale global state across requests causes a use-after-free in the upload progress feature, which leaks memory or runs code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are rated &lt;strong&gt;CVSS 9.2&lt;/strong&gt; (watchTowr rated it 10.0). Attackers could read passwords, tokens, API keys and certificates straight out of PHP process memory, including data from other people's requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who found it:&lt;/strong&gt; AI. Ryan Austin at Calif found it in an August 2026 audit, part of OpenAI's "Patch the Planet" project, using GPT-5.6 Sol. Wiz Research found it independently. A PHP core developer had spotted the out-of-memory error back in a 2024 audit, and it was never fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it got out early:&lt;/strong&gt; details leaked on Hacker News on October 20, just under two days before the patch. Amazon Threat Intelligence saw the first exploit attempt on October 17. Cloudflare's CEO said they saw thousands of attempts before the announcement and millions after. Dutch, Finnish and French government sites went offline, and the NSA advised sites that couldn't update to go offline too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# fixed: 8.5.12, 8.4.27, 8.3.36, 8.2.35 (source-only backports for 7.4, 8.0, 8.1)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Apache, PHP-FPM and any long-running workers so the old code is actually gone from memory. There is no complete workaround: upgrading PHP is the only fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. LiteLLM 1.107.0: a backdoored Python package
&lt;/h2&gt;

&lt;p&gt;LiteLLM is a Python proxy library for LLM providers with about 23 million weekly downloads. On October 21, a malicious &lt;strong&gt;1.107.0&lt;/strong&gt; was live on PyPI from 11:39 to 13:20 UTC (Docker images too). Wiz counted about &lt;strong&gt;49,000 downloads&lt;/strong&gt; in those two hours.&lt;/p&gt;

&lt;p&gt;The attackers used stolen GitHub CLI OAuth tokens from three engineers to run the release workflow against a lookalike fork, &lt;code&gt;Berri-AI/litellm&lt;/code&gt; (the real org is &lt;code&gt;BerriAI&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The clever, nasty part: it dropped three files into &lt;code&gt;site-packages&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cook.pth&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cook.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sitecustomize.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;.pth&lt;/code&gt; file in &lt;code&gt;site-packages&lt;/code&gt; runs &lt;strong&gt;every time Python starts&lt;/strong&gt;. So the stealer ran even if you never imported litellm, and &lt;code&gt;pip uninstall litellm&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; remove these files. It went after browser passwords and cookies, AWS/GCP/Azure credentials, OpenAI/Anthropic/Cohere API keys, SSH and GPG keys, and shell history, and sent them to the lookalike domain &lt;code&gt;litellm[.]dev&lt;/code&gt;. About 4.5 hours later the attacker was already testing stolen AWS keys for Bedrock access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check every Python environment you have:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import site; print(site.getsitepackages())"&lt;/span&gt;
&lt;span class="c"&gt;# look in each path printed above:&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &amp;lt;site-packages-path&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(cook\.pth|cook\.py|sitecustomize\.py)$'&lt;/span&gt;
pip show litellm | &lt;span class="nb"&gt;grep &lt;/span&gt;Version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find them: delete all three files, go back to &lt;strong&gt;1.106.2&lt;/strong&gt;, and rotate every credential that machine could see, from a clean machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. GitLab CVE-2026-110264: a 10.0 going back to 2014
&lt;/h2&gt;

&lt;p&gt;An integer overflow while compiling a crafted regular expression lets an unauthenticated user run code on a self-managed GitLab server. &lt;strong&gt;CVSS 10.0.&lt;/strong&gt; Affected versions go all the way back to GitLab 7.0, released in 2014. Censys counted 81,820 internet-visible GitLab hosts.&lt;/p&gt;

&lt;p&gt;Fixed releases: &lt;strong&gt;19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10, 18.11.13&lt;/strong&gt;. No workaround.&lt;/p&gt;

&lt;p&gt;The trap: if you patched on &lt;strong&gt;October 12&lt;/strong&gt;, you're still vulnerable. That was CVE-2026-97229, a &lt;em&gt;different&lt;/em&gt; 10.0 regex bug from the same researcher. GitLab.com and GitLab Dedicated are already patched.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. AI agents inside bank accounts
&lt;/h2&gt;

&lt;p&gt;Axios reported on October 22 that third-party AI agents were logging into US bank customer accounts, sometimes with the customers' own usernames and passwords, and some took actions that "appear to go beyond what anyone asked them to do." Banks often can't tell whether a login is a person or a machine.&lt;/p&gt;

&lt;p&gt;Morgan Stanley is blocking outside agents; JPMorgan, Bank of America and Citi are restricting them. (Some of this reporting comes from anonymous sources.)&lt;/p&gt;

&lt;p&gt;The dev lesson: if you build agents, use proper delegated, scoped access (OAuth, read-only where possible). Never ask users for raw bank credentials. As a user: never hand your bank password to an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. eMudhra: the private key was 4
&lt;/h2&gt;

&lt;p&gt;eMudhra is an Indian certificate authority. Eight of its intermediate CA private keys could be computed straight from the public certificates. The keys were &lt;strong&gt;4, 56, 66, 115, 155, 156, 230 and 237&lt;/strong&gt;. A P-384 private key should be a random 384-bit number (about 3.9 × 10¹¹⁵ possible values). These all fit in one byte. Scott Helme recovered all eight on a laptop in under a second.&lt;/p&gt;

&lt;p&gt;eMudhra said a flaw in its April 2025 key generation ceremony produced a second set of weak keys alongside the strong ones. Chrome blocked them on October 17 about 100 minutes after the report, and Firefox, Apple and Microsoft followed. No fraudulent certificates were found in Certificate Transparency logs, but anyone with those keys could have signed a certificate for any domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; update your OS, browser and runtimes. Go shipped fixes in &lt;strong&gt;1.27.3 and 1.26.10&lt;/strong&gt; (CVE-2026-111943), and certifi, pip, uv and Electron all got patches. If you ship a Go binary or a bundled CA store, rebuild it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP → 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, then restart PHP-FPM / Apache / workers&lt;/li&gt;
&lt;li&gt;Search every venv, container and CI image for &lt;code&gt;cook.pth&lt;/code&gt;; LiteLLM → 1.106.2; rotate keys&lt;/li&gt;
&lt;li&gt;Self-managed GitLab → 19.5.1 / 19.4.3 / 19.3.6 / 19.2.9 / 19.1.9 / 19.0.10 / 18.11.13&lt;/li&gt;
&lt;li&gt;Go → 1.27.3 or 1.26.10; update certifi, pip, uv, Electron; rebuild bundled CA stores&lt;/li&gt;
&lt;li&gt;No raw bank credentials in any AI agent you build or use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one hit you hardest this week? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;📺 Full video: &lt;a href="https://youtu.be/KNXtuCbvxDw" rel="noopener noreferrer"&gt;https://youtu.be/KNXtuCbvxDw&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PHP2Shell: &lt;a href="https://www.php2shell.com/" rel="noopener noreferrer"&gt;https://www.php2shell.com/&lt;/a&gt; · &lt;a href="https://en.wikipedia.org/wiki/PHP2Shell" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/PHP2Shell&lt;/a&gt; · &lt;a href="https://www.php.net/" rel="noopener noreferrer"&gt;https://www.php.net/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CNBC: &lt;a href="https://www.cnbc.com/2026/10/22/php2shell-vulnerability-what-is-it-and-is-your-data-at-risk.html" rel="noopener noreferrer"&gt;https://www.cnbc.com/2026/10/22/php2shell-vulnerability-what-is-it-and-is-your-data-at-risk.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LiteLLM incident report: &lt;a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026" rel="noopener noreferrer"&gt;https://docs.litellm.ai/blog/security-incident-oct-21-2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wiz Research: &lt;a href="https://www.wiz.io/blog/litellm-compromised-steals-credentials" rel="noopener noreferrer"&gt;https://www.wiz.io/blog/litellm-compromised-steals-credentials&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Datadog Security Labs: &lt;a href="https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/" rel="noopener noreferrer"&gt;https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;BleepingComputer (GitLab): &lt;a href="https://www.bleepingcomputer.com/news/security/gitlab-warns-of-new-max-severity-code-execution-vulnerability/" rel="noopener noreferrer"&gt;https://www.bleepingcomputer.com/news/security/gitlab-warns-of-new-max-severity-code-execution-vulnerability/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Axios on X: &lt;a href="https://x.com/axios/status/2113198996759957511" rel="noopener noreferrer"&gt;https://x.com/axios/status/2113198996759957511&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Security Blog (eMudhra): &lt;a href="https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/" rel="noopener noreferrer"&gt;https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Next Web (eMudhra): &lt;a href="https://thenextweb.com/news/emudhra-certificates-weak-keys-browsers-block" rel="noopener noreferrer"&gt;https://thenextweb.com/news/emudhra-certificates-weak-keys-browsers-block&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>php</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>iPhone Duo Is Out: Which Storage Should You Buy? 256GB vs 512GB vs 1TB vs 2TB</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Sat, 24 Oct 2026 03:32:07 +0000</pubDate>
      <link>https://dev.to/rainytechlab/iphone-duo-is-out-which-storage-should-you-buy-256gb-vs-512gb-vs-1tb-vs-2tb-3j9o</link>
      <guid>https://dev.to/rainytechlab/iphone-duo-is-out-which-storage-should-you-buy-256gb-vs-512gb-vs-1tb-vs-2tb-3j9o</guid>
      <description>&lt;p&gt;&lt;em&gt;AI-assisted disclosure: I used an AI assistant to help structure this write-up. Prices and specs come from Apple's iPhone Duo page, tech specs and Newsroom (linked at the end). Not sponsored by Apple.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer: most people should buy the 256GB iPhone Duo. If you film a lot of 4K or keep phones for years, get the 512GB.&lt;/strong&gt; The 1TB is for working creators, and the 2TB is money almost nobody will use.&lt;/p&gt;

&lt;p&gt;The iPhone Duo has been in stores since &lt;strong&gt;Friday, October 23, 2026&lt;/strong&gt;. The base model is $1,999, but the one you see in every ad can cost $3,199. That's a $1,200 gap for the exact same phone: same screen, same chip, same cameras. You only pay more for storage.&lt;/p&gt;

&lt;p&gt;📺 Full video (8:04): &lt;a href="https://youtu.be/0DHT--R3RiI" rel="noopener noreferrer"&gt;https://youtu.be/0DHT--R3RiI&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four prices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;256GB:&lt;/strong&gt; $1,999&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;512GB:&lt;/strong&gt; $2,199 (+$200)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1TB:&lt;/strong&gt; $2,599 (+$400)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2TB:&lt;/strong&gt; $3,199 (+$600)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice how the steps grow. Night Sky and Star White cost the same. Monthly payments start at about $83.29/month over 24 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What every iPhone Duo gets (storage changes none of this)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;7.6-inch inner display&lt;/strong&gt;, which Apple says is 50% bigger than the iPhone 18 Pro Max, plus a &lt;strong&gt;5.4-inch outer display&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5.2 mm thin when open&lt;/strong&gt;, the thinnest iPhone Apple has made&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A20 Pro chip&lt;/strong&gt; (same as iPhone 18 Pro)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two 48MP Fusion cameras&lt;/strong&gt;, 4K at up to 120 fps in Dolby Vision&lt;/li&gt;
&lt;li&gt;Up to &lt;strong&gt;44 hours&lt;/strong&gt; of video playback on the outer display&lt;/li&gt;
&lt;li&gt;Titanium frame and hinge cover, IP68&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;50% charge in ~20 minutes&lt;/strong&gt; with a strong wired charger, MagSafe up to 25W&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;USB-C up to 10 Gb/s&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest iPhone Duo is just as fast as the most expensive one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much storage do you actually need?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;iOS and built-in apps take about &lt;strong&gt;12–24 GB&lt;/strong&gt;, so the 256GB model leaves roughly &lt;strong&gt;230 GB&lt;/strong&gt; for you.&lt;/li&gt;
&lt;li&gt;The camera is where storage disappears. 4K video takes hundreds of megabytes per minute even in Apple's efficient format, and a few long 4K clips from a trip can fill dozens of gigabytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no memory card slot.&lt;/strong&gt; Whatever you buy, you live with for years.&lt;/li&gt;
&lt;li&gt;Games and offline movies add up, especially on a 7.6-inch screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iCloud&lt;/strong&gt; can keep full-size photos online and smaller copies on the phone. That alone saves many people from buying more storage, but it's a monthly bill and needs a good connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick check:&lt;/strong&gt; Settings → General → iPhone Storage. If you use less than 150 GB today, the 256GB model is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  My pick for each buyer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;256GB ($1,999): most people.&lt;/strong&gt; Browsing, messaging, video, normal photos, especially if you already pay for iCloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;512GB ($2,199): the sweet spot.&lt;/strong&gt; Double the space for $200, the cheapest gigabytes in the lineup. Pick it if you film a lot of 4K, play big games, skip iCloud, or keep phones 4–5 years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1TB ($2,599): creators&lt;/strong&gt; who record long 4K every week or edit on the phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2TB ($3,199): skip it.&lt;/strong&gt; $1,200 over the base model is almost a second phone. With USB-C at 10 Gb/s, a cheap external drive beats paying $600 more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Face ID.&lt;/strong&gt; You unlock with Touch ID in the side button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No telephoto lens.&lt;/strong&gt; Zoom tops out at 2x optical quality. The iPhone 18 Pro has a real 48MP telephoto for $800 less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eSIM only.&lt;/strong&gt; No physical SIM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Pencil support comes later this year&lt;/strong&gt;, not at launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Siri AI is in beta&lt;/strong&gt;, English first, not in the EU at launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's Apple's first-generation foldable.&lt;/strong&gt; If you're unsure, wait for real long-term reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you love zooming at concerts or sports, want Face ID, or want the best value, the iPhone 18 Pro (from $1,199) is the better buy. If you want a tablet-sized screen that folds into your pocket, nothing else from Apple does that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pay less
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trade-in credit up to &lt;strong&gt;$885&lt;/strong&gt; (iPhone 13 or newer)&lt;/li&gt;
&lt;li&gt;Carrier credits at Apple up to &lt;strong&gt;$1,200&lt;/strong&gt; (AT&amp;amp;T)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Upgrade&lt;/strong&gt; lease from about &lt;strong&gt;$57.99/month&lt;/strong&gt;. Compare the final price, not just the monthly one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final answer
&lt;/h2&gt;

&lt;p&gt;256GB for most people. 512GB if you film a lot or keep phones for years. 1TB only for pro creators. 2TB is money you'll almost certainly never use.&lt;/p&gt;

&lt;p&gt;Which one are you getting? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;📺 Full video: &lt;a href="https://youtu.be/0DHT--R3RiI" rel="noopener noreferrer"&gt;https://youtu.be/0DHT--R3RiI&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Apple, iPhone Duo: &lt;a href="https://www.apple.com/iphone-duo/" rel="noopener noreferrer"&gt;https://www.apple.com/iphone-duo/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, iPhone Duo tech specs: &lt;a href="https://www.apple.com/iphone-duo/specs/" rel="noopener noreferrer"&gt;https://www.apple.com/iphone-duo/specs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple Newsroom: &lt;a href="https://www.apple.com/newsroom/2026/09/apple-unveils-iphone-duo/" rel="noopener noreferrer"&gt;https://www.apple.com/newsroom/2026/09/apple-unveils-iphone-duo/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>apple</category>
      <category>ios</category>
      <category>mobile</category>
      <category>hardware</category>
    </item>
    <item>
      <title>How an AI Agent Taught NVIDIA's $399 Robot Duck to Walk: Prompt to Policy in 7 Steps</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Sat, 24 Oct 2026 03:31:44 +0000</pubDate>
      <link>https://dev.to/rainytechlab/how-an-ai-agent-taught-nvidias-399-robot-duck-to-walk-prompt-to-policy-in-7-steps-7d0</link>
      <guid>https://dev.to/rainytechlab/how-an-ai-agent-taught-nvidias-399-robot-duck-to-walk-prompt-to-policy-in-7-steps-7d0</guid>
      <description>&lt;p&gt;&lt;em&gt;AI-assisted disclosure: I used an AI assistant to help structure this write-up. This is **not&lt;/em&gt;* a hands-on test. Everything here comes from NVIDIA (the keynote and its RTX Spark demo), Pollen Robotics / Hugging Face, and news coverage linked at the end.*&lt;/p&gt;

&lt;p&gt;At NVIDIA's GTC Berlin keynote on &lt;strong&gt;October 21, 2026&lt;/strong&gt;, Jensen Huang gave four tiny robots one command: "Take a nap." Three dropped instantly. The fourth kept walking for a few steps before tipping over.&lt;/p&gt;

&lt;p&gt;The fall is funny, but it isn't the story. The story is &lt;strong&gt;how they learned to walk at all&lt;/strong&gt;: thousands of virtual copies trained in a world that an AI agent built on a laptop.&lt;/p&gt;

&lt;p&gt;📺 Full video (8:00): &lt;a href="https://youtu.be/R1VtPIjb6Es" rel="noopener noreferrer"&gt;https://youtu.be/R1VtPIjb6Es&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Microduck is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A tiny walking robot from &lt;strong&gt;Pollen Robotics&lt;/strong&gt;, the robotics team Hugging Face bought in April 2025.&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;25 cm (10 in) tall&lt;/strong&gt;, under 2 lb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15 motors&lt;/strong&gt; (legs, neck, head), a front camera, a depth sensor and two IMUs.&lt;/li&gt;
&lt;li&gt;Walks, sits, crouches, catches its balance when pushed, and gets back up on its own. Seven behaviours come trained out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$399&lt;/strong&gt; pre-order (opened Aug 27), now &lt;strong&gt;sold out&lt;/strong&gt;. First shipments expected by Christmas in North America and Europe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The training code is open source&lt;/strong&gt;, so owners can teach it new tricks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline: prompt to walking robot in 7 steps
&lt;/h2&gt;

&lt;p&gt;NVIDIA's demo ran this stack on an &lt;strong&gt;RTX Spark&lt;/strong&gt; laptop (N1X chip, "one petaflop" per Jensen), bursting big jobs to the cloud:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent harness:&lt;/strong&gt; Hermes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; Nemotron and OpenAI's GPT-6 Astra&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; FreeCAD, Blender, Omniverse SimReady, Isaac Sim, the Newton physics engine, Isaac Lab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt.&lt;/strong&gt; Describe the course and the skill: "plan an obstacle course to teach Microduck how to walk."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; Hermes + Nemotron lay out the course as a task-by-task workflow, listing every object, and even generate a concept image first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; GPT-6 Astra models each 3D object in FreeCAD, applies materials in Blender, and adds real physical properties from Omniverse SimReady libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulate.&lt;/strong&gt; Astra composes the course in Isaac Sim, rigs the duck's joints, actuators and links, and simulates movement with Newton.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train.&lt;/strong&gt; In the cloud, &lt;strong&gt;thousands of Microducks train in parallel in Isaac Lab&lt;/strong&gt; with reinforcement learning. Each virtual duck tries a move, gets a score, and the policy is adjusted. Omniverse also simulates the cameras, so the duck learns to see and avoid obstacles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate.&lt;/strong&gt; The policy comes back to the RTX Spark, where Nemotron and Isaac Sim test it with many robots running at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy.&lt;/strong&gt; The validated policy goes onto the real duck.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Jensen's line from the keynote sums up the approach: "The way you build an agent is you ask an agent to build it for you."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sim-to-real matters
&lt;/h2&gt;

&lt;p&gt;Pollen shows the same idea in its own footage: a move learned in simulation runs on the real robot, and &lt;strong&gt;when a move fails in simulation, it fails the same way on the real robot&lt;/strong&gt;. That's the whole point. You crash the virtual duck a thousand times, not the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The nap is the honest part of the demo.&lt;/strong&gt; A simulation is never exactly the real world. Real floors, lights and noise differ, so robots still slip up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The laptop didn't do the heavy lifting.&lt;/strong&gt; Training ran on NVIDIA supercomputers in the cloud. The RTX Spark planned the work and checked the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why developers should care
&lt;/h2&gt;

&lt;p&gt;This is NVIDIA's "physical AI" pitch in miniature: an agent builds the world, the cloud trains the policy, a local machine evaluates it, and the robot runs it. The same keynote announced a 31-degree-of-freedom reference humanoid (Agile ONE SN) built on Isaac GR00T, and Reuters reported Agile Robots plans to produce 1,000 to 2,000 small humanoids next year with NVIDIA.&lt;/p&gt;

&lt;p&gt;A $399 open-source duck is the cheapest way to see that recipe end to end.&lt;/p&gt;

&lt;p&gt;Would you buy one and teach it new tricks? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;📺 Full video: &lt;a href="https://youtu.be/R1VtPIjb6Es" rel="noopener noreferrer"&gt;https://youtu.be/R1VtPIjb6Es&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NVIDIA GTC Berlin 2026 keynote: &lt;a href="https://www.youtube.com/watch?v=PvFjbPYEOZI" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=PvFjbPYEOZI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NVIDIA, "From Prompt to Walking Robot With NVIDIA RTX Spark": &lt;a href="https://www.youtube.com/watch?v=VvJai6HGfyE" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=VvJai6HGfyE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NVIDIA blog, keynote live updates: &lt;a href="https://blogs.nvidia.com/blog/gtc-berlin-2026-keynote/" rel="noopener noreferrer"&gt;https://blogs.nvidia.com/blog/gtc-berlin-2026-keynote/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pollen Robotics, "Meet Microduck": &lt;a href="https://www.youtube.com/watch?v=reiTh7K4KSc" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=reiTh7K4KSc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pollen Robotics, sim to real: &lt;a href="https://www.youtube.com/watch?v=szW7N_7B3tU" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=szW7N_7B3tU&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reuters (via KSL): &lt;a href="https://www.ksl.com/article/51639111/nvidias-huang-hails-europes-ai-potential-flanked-by-robot-ducks" rel="noopener noreferrer"&gt;https://www.ksl.com/article/51639111/nvidias-huang-hails-europes-ai-potential-flanked-by-robot-ducks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Register: &lt;a href="https://www.theregister.com/ai-and-ml/2026/10/21/jensen-huang-sends-in-the-robot-ducks-to-sell-nvidias-ai-everywhere-vision/5304472" rel="noopener noreferrer"&gt;https://www.theregister.com/ai-and-ml/2026/10/21/jensen-huang-sends-in-the-robot-ducks-to-sell-nvidias-ai-everywhere-vision/5304472&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>robotics</category>
      <category>nvidia</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Is FilmCraft 100% Free? I Scored the Open-Source Premiere Pro Clone in 10 Rounds</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Sat, 24 Oct 2026 02:57:56 +0000</pubDate>
      <link>https://dev.to/rainytechlab/is-filmcraft-100-free-i-scored-the-open-source-premiere-pro-clone-in-10-rounds-3g5k</link>
      <guid>https://dev.to/rainytechlab/is-filmcraft-100-free-i-scored-the-open-source-premiere-pro-clone-in-10-rounds-3g5k</guid>
      <description>&lt;p&gt;&lt;em&gt;AI-assisted disclosure: I used an AI assistant to help structure this write-up. Every number comes from my own runs on my Mac or from the sources linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer: yes, FilmCraft is free.&lt;/strong&gt; It's open source (MIT OR Apache-2.0), it rebuilds Adobe Premiere Pro from scratch in pure Rust, and it costs $0. Premiere Pro costs US$22.99/month on the annual plan.&lt;/p&gt;

&lt;p&gt;But is a free Premiere clone actually &lt;em&gt;usable&lt;/em&gt;? I gave FilmCraft 0.9.0 real jobs on a Mac mini M4 (16 GB) and scored it against Premiere in 10 rounds.&lt;/p&gt;

&lt;p&gt;📺 Full video (5:38): &lt;a href="https://youtu.be/0moxAPMMego" rel="noopener noreferrer"&gt;https://youtu.be/0moxAPMMego&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What FilmCraft is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;clean-room reimplementation of Premiere Pro&lt;/strong&gt;: same panels, workspaces, tools and keyboard shortcuts (with shortcut presets for Premiere, Final Cut Pro and Avid).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure Rust, no FFmpeg inside.&lt;/strong&gt; It ships its own H.264, HEVC, ProRes and AAC codecs.&lt;/li&gt;
&lt;li&gt;From the ArtCraft team. The repo was created Sep 30, 2026 and had &lt;strong&gt;14,962 stars and 6,249 forks&lt;/strong&gt; 19 days later.&lt;/li&gt;
&lt;li&gt;Runs on macOS, Windows, Linux and in the browser (WebAssembly).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;650+ engine commands, a CLI and an MCP server&lt;/strong&gt;, so an AI agent can drive the editor. This is the part developers should care about most.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The scorecard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;th&gt;Score (FilmCraft–Premiere)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Interface &amp;amp; shortcuts&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;td&gt;1–1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Editing &amp;amp; trimming&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;td&gt;2–2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Green-screen keying&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;td&gt;3–3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Effects &amp;amp; transitions&lt;/td&gt;
&lt;td&gt;Premiere&lt;/td&gt;
&lt;td&gt;3–4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Color grading&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Audio&lt;/td&gt;
&lt;td&gt;Premiere&lt;/td&gt;
&lt;td&gt;4–6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Captions &amp;amp; AI&lt;/td&gt;
&lt;td&gt;Premiere&lt;/td&gt;
&lt;td&gt;4–7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Formats &amp;amp; export&lt;/td&gt;
&lt;td&gt;Premiere&lt;/td&gt;
&lt;td&gt;4–8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Projects &amp;amp; plugins&lt;/td&gt;
&lt;td&gt;Premiere&lt;/td&gt;
&lt;td&gt;4–9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10. Price &amp;amp; freedom&lt;/td&gt;
&lt;td&gt;FilmCraft&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5–9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Premiere wins on depth. FilmCraft wins on price.&lt;/p&gt;

&lt;p&gt;Honesty note: Premiere was &lt;strong&gt;not&lt;/strong&gt; installed on my Mac. The Premiere side of each round uses Adobe's own published docs and product pages. The FilmCraft side is real runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer angle: a whole edit from one script
&lt;/h2&gt;

&lt;p&gt;"Every menu item, button, slider and drag in FilmCraft is a command" (that's from the README). So I built a 15-second 4K coffee-shop ad without touching the timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new sequence, 5 clip placements, 4 cross dissolves&lt;/li&gt;
&lt;li&gt;a Lumetri grade on all 5 clips (9 settings each)&lt;/li&gt;
&lt;li&gt;2 titles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's &lt;strong&gt;58 commands, run from one script in 0.01 s&lt;/strong&gt;. The same command surface is exposed through the CLI and an MCP server, which means an LLM agent can assemble, grade and export a video. Premiere can't do this without a plugin SDK and a lot of glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real numbers from my Mac mini M4
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build the 15 s 4K ad (58 commands)&lt;/td&gt;
&lt;td&gt;0.01 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One 4K frame preview&lt;/td&gt;
&lt;td&gt;~0.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export 15 s 4K H.264, software&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;82 s&lt;/strong&gt; (4.4 fps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export 15 s 4K H.264, hardware encoding on&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;131 s&lt;/strong&gt; (2.8 fps), slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export 13 s 1080p30 meme, H.264&lt;/td&gt;
&lt;td&gt;27.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to FCP7 XML (what Premiere imports)&lt;/td&gt;
&lt;td&gt;valid: 7 clip items, 4 transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yes, hardware encoding was &lt;em&gt;slower&lt;/em&gt; than software. Lumetri, keys and export rendering still run on the CPU in this build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green-screen keying: a 1:1 copy, with a catch
&lt;/h2&gt;

&lt;p&gt;FilmCraft's Ultra Key has the &lt;strong&gt;same 18 parameters with the same defaults&lt;/strong&gt; as Premiere's (Transparency 45, Highlight 10, Shadow 50, Tolerance 50, Pedestal 10, and so on).&lt;/p&gt;

&lt;p&gt;But on my green-screen clip (a tourist with a map, backdrop rgb 102,221,9) the &lt;strong&gt;default key colour didn't key it at all&lt;/strong&gt;. Setting the key colour to the backdrop and switching to "Aggressive" keyed it cleanly. Every meme in the video was made this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can't open &lt;code&gt;.prproj&lt;/code&gt; files.&lt;/strong&gt; This is the team's main beta blocker. Importing a Premiere sequence via XML took 97 s in their own report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No plugins&lt;/strong&gt;: no VST3 / Audio Units, no OpenFX, no Premiere/After Effects plug-in API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto captions don't work out of the box on Mac.&lt;/strong&gt; Offline Whisper transcription exists, but the macOS release build says model downloads aren't available ("built without speech-download").&lt;/li&gt;
&lt;li&gt;H.264 is 8-bit 4:2:0 only, no camera RAW, and no AV1 export.&lt;/li&gt;
&lt;li&gt;The team's own parity report: &lt;strong&gt;~86% of Premiere's features, but only ~56% "ready for real work"&lt;/strong&gt;. Status: alpha.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pro client work?&lt;/strong&gt; Stay on Premiere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memes, shorts, school projects, small ads?&lt;/strong&gt; FilmCraft works, for $0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building an AI video pipeline?&lt;/strong&gt; FilmCraft's command/CLI/MCP surface is the most interesting thing here, and it's open source, so you can read exactly what each command does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would you switch? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;📺 Full video with all 10 rounds: &lt;a href="https://youtu.be/0moxAPMMego" rel="noopener noreferrer"&gt;https://youtu.be/0moxAPMMego&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;FilmCraft repo and README: &lt;a href="https://github.com/storytold/filmcraft" rel="noopener noreferrer"&gt;https://github.com/storytold/filmcraft&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FilmCraft parity report (vs Premiere Pro 26.5.2): &lt;a href="https://github.com/storytold/filmcraft/blob/main/docs/target-app-parity.md" rel="noopener noreferrer"&gt;https://github.com/storytold/filmcraft/blob/main/docs/target-app-parity.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Adobe Premiere Pro pricing: &lt;a href="https://www.adobe.com/products/premiere.html" rel="noopener noreferrer"&gt;https://www.adobe.com/products/premiere.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Adobe Help Center, "Ultra Key effect parameters" (updated Jan 7, 2026)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>rust</category>
      <category>videoediting</category>
      <category>review</category>
    </item>
    <item>
      <title>PhotoCraft Review: I Tested the Free, Open-Source Photoshop Alternative With 4 Real Edits</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:27:24 +0000</pubDate>
      <link>https://dev.to/rainytechlab/photocraft-review-i-tested-the-free-open-source-photoshop-alternative-with-4-real-edits-28j1</link>
      <guid>https://dev.to/rainytechlab/photocraft-review-i-tested-the-free-open-source-photoshop-alternative-with-4-real-edits-28j1</guid>
      <description>&lt;p&gt;Photoshop costs &lt;strong&gt;$22.99 a month&lt;/strong&gt;. PhotoCraft costs &lt;strong&gt;$0&lt;/strong&gt;, it's open source, and in under three weeks it passed &lt;strong&gt;66,000 stars on GitHub&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I put it to work on my Mac mini M4 with four real edits, the kind most of us actually do: cutting someone out, a caption meme, fixing a dull photo, and text for a meme video. Here is what worked, step by step, and what it still can't do.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/O4necNCA5D4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;▶ &lt;strong&gt;Full video:&lt;/strong&gt; &lt;a href="https://youtu.be/O4necNCA5D4" rel="noopener noreferrer"&gt;https://youtu.be/O4necNCA5D4&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PhotoCraft?
&lt;/h2&gt;

&lt;p&gt;PhotoCraft is a free copy of Photoshop, rebuilt from scratch in the &lt;strong&gt;Rust&lt;/strong&gt; language. It's part of ArtCraft, a family of open-source clones of creative apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open source:&lt;/strong&gt; all the code is public on GitHub, so anyone can read it, fix it or improve it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs everywhere:&lt;/strong&gt; Mac, Windows, Linux, and even in your web browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; no account, no subscription, no cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opens real PSD files&lt;/strong&gt; with your layers still editable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install and quick tour
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;getartcraft.com&lt;/strong&gt; (or the GitHub releases page) and pick the file for your computer.&lt;/li&gt;
&lt;li&gt;On a Mac, open the DMG and drag PhotoCraft into Applications.&lt;/li&gt;
&lt;li&gt;Open it. If you've used Photoshop, it will feel familiar: &lt;strong&gt;67 tools&lt;/strong&gt; on the left (move, select, crop, brush, text…) and &lt;strong&gt;layers&lt;/strong&gt; on the right. Every edit sits on its own layer, like sheets of glass.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Edit 1: Cut out anyone (The Rock eyebrow meme)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open a frame from the meme.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Command + F&lt;/strong&gt; and search for &lt;strong&gt;Subject&lt;/strong&gt;. PhotoCraft finds the person by itself, in about a tenth of a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer → Layer Mask → Reveal Selection.&lt;/strong&gt; The background is gone.&lt;/li&gt;
&lt;li&gt;Open any background photo, &lt;strong&gt;Select All → Copy&lt;/strong&gt;, and paste it into the meme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer → Arrange → Send to Back&lt;/strong&gt;, so it sits behind him.&lt;/li&gt;
&lt;li&gt;Add a caption with the &lt;strong&gt;Type&lt;/strong&gt; tool, then give it a &lt;strong&gt;Stroke&lt;/strong&gt; from the fx button so it pops on any background.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: a brand-new meme, and every layer is still editable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edit 2: The classic caption meme (Surprised Pikachu)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open a frame from the meme video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image → Canvas Size.&lt;/strong&gt; Anchor at the bottom, make it taller, and fill with white. Now there's room on top for the joke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt; tool, black text, write the caption. Tip: the best captions fit in two lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File → Export&lt;/strong&gt; as a PNG, and post it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Edit 3: Make a dull photo pop, the pro way
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Layer → New Adjustment Layer → Curves.&lt;/strong&gt; Bend the line into an S shape for more contrast.&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Vibrance&lt;/strong&gt; adjustment layer to bring the colours back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Adjustment layers never change your original photo: turn them off and it's untouched. The same two layers work on selfies, product photos and thumbnails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edit 4: Text for a meme video
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File → New&lt;/strong&gt;, background &lt;strong&gt;Transparent&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Write your caption and give it a &lt;strong&gt;stroke and a drop shadow&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Export as &lt;strong&gt;PNG&lt;/strong&gt;. The empty parts stay see-through.&lt;/li&gt;
&lt;li&gt;Drop that PNG on top of any clip in your video editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bonus: scripts and AI agents can drive it
&lt;/h2&gt;

&lt;p&gt;Every button in PhotoCraft is also a command, over &lt;strong&gt;500&lt;/strong&gt; of them. So scripts, and even AI agents, can run the same edits, with the same engine and the same results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PhotoCraft is still &lt;strong&gt;early alpha&lt;/strong&gt;. The team says it's not yet a Photoshop replacement for daily professional work.&lt;/li&gt;
&lt;li&gt;There's &lt;strong&gt;no generative AI&lt;/strong&gt; yet, some tools are missing, and you will hit bugs.&lt;/li&gt;
&lt;li&gt;But for &lt;strong&gt;memes, thumbnails and quick photo fixes&lt;/strong&gt;, it already does the job, for free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Price check (October 18, 2026)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adobe Photoshop:&lt;/strong&gt; US$22.99/month (annual plan, billed monthly, adobe.com)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PhotoCraft:&lt;/strong&gt; free, open source&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full video: &lt;a href="https://youtu.be/O4necNCA5D4" rel="noopener noreferrer"&gt;https://youtu.be/O4necNCA5D4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Download PhotoCraft: &lt;a href="https://getartcraft.com/apps/photocraft" rel="noopener noreferrer"&gt;https://getartcraft.com/apps/photocraft&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/storytold/photocraft" rel="noopener noreferrer"&gt;https://github.com/storytold/photocraft&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would you switch from Photoshop? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not sponsored by or affiliated with ArtCraft or Adobe. Adobe and Photoshop are trademarks of Adobe Inc.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance, from my own video and real tests.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>design</category>
      <category>tutorial</category>
      <category>rust</category>
    </item>
    <item>
      <title>I Tested 3 Free, Open-Source Adobe Clones on My Mac. Here Are the Real Numbers.</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:58:19 +0000</pubDate>
      <link>https://dev.to/rainytechlab/i-tested-3-free-open-source-adobe-clones-on-my-mac-here-are-the-real-numbers-13mi</link>
      <guid>https://dev.to/rainytechlab/i-tested-3-free-open-source-adobe-clones-on-my-mac-here-are-the-real-numbers-13mi</guid>
      <description>&lt;p&gt;ArtCraft just open-sourced free Rust rebuilds of Photoshop, Premiere, After Effects and ten more apps. PhotoCraft alone went past 61,000 GitHub stars in about two weeks.&lt;/p&gt;

&lt;p&gt;Stars don't edit videos, though. So I installed three of the apps on my Mac mini M4 (16 GB) and gave them real work. Every number below comes from a log I saved on October 15, 2026.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/20zJ3RSezlY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What ArtCraft actually is
&lt;/h2&gt;

&lt;p&gt;ArtCraft is a family of desktop apps that copy the Adobe and Microsoft playbook, released under the Apache 2.0 license:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PhotoCraft&lt;/strong&gt;: Photoshop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VectorCraft&lt;/strong&gt;: Illustrator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FilmCraft&lt;/strong&gt;: Premiere Pro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EffectCraft&lt;/strong&gt;: After Effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LightCraft&lt;/strong&gt;: Lightroom&lt;/li&gt;
&lt;li&gt;Plus clones of InDesign, Figma, Word, PowerPoint, Excel, Pro Tools and AutoCAD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I took my first screenshot the site listed 12 apps. By the time I finished testing, it listed 13. They run on Mac, Windows, Linux and in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The price gap:&lt;/strong&gt; Adobe Creative Cloud Pro is US$69.99/month on adobe.com (about $840 a year). ArtCraft is $0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; most of these repos were created on September 30, 2026. Every app I checked is labelled early alpha or "in development". So the real question is whether you can use them today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 1: PhotoCraft photo edit (0.23 s)
&lt;/h2&gt;

&lt;p&gt;PhotoCraft looks almost exactly like Photoshop: same toolbar, same panels, same layers. The interesting part is that every action is a command, and the app ships its own MCP server. An AI agent or a small script can drive the real app while you watch.&lt;/p&gt;

&lt;p&gt;The edit: auto tone, remove background, yellow fill layer behind the subject, two text layers, drop shadow. From the command line (photocraft-cli 0.9.0), the whole thing took &lt;strong&gt;0.23 seconds&lt;/strong&gt; and saved a real layered PSD with every layer still editable.&lt;/p&gt;

&lt;p&gt;The log, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"image.autoTone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"layer.removeBackground"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"bounds"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="mi"&gt;171&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;664&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"layer.newFillLayer.solidColor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"type.create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"bounds"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="mi"&gt;838&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;244&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1155&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;351&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"layer.layerStyle.dropShadow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.23&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; Remove Background left a blue fringe in the subject's hair from the old backdrop. Photoshop's masking still wins here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 2: batch-edit 20 product photos (1.75 s)
&lt;/h2&gt;

&lt;p&gt;The boring test that actually saves time. Picture a small online shop fixing 20 product photos every week. I gave PhotoCraft one action list: auto tone, more vibrance, unsharp mask, resize to 1080 px.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"image.autoTone"&lt;/span&gt;&lt;span class="p"&gt;,{}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"image.adjustments.vibrance"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;&lt;span class="nl"&gt;"vibrance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"saturation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"filter.sharpen.unsharpMask"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"radius"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"image.imageSize"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;}]]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;photocraft-cli batch &lt;span class="nt"&gt;--actions&lt;/span&gt; actions.json &lt;span class="nt"&gt;--in&lt;/span&gt; batch-in &lt;span class="nt"&gt;--out&lt;/span&gt; batch-out &lt;span class="nt"&gt;--format&lt;/span&gt; jpg &lt;span class="nt"&gt;--quality&lt;/span&gt; 90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;20 succeeded, 0 failed, 1.75 seconds.&lt;/strong&gt; In Photoshop you'd record an action for this. Here it's one line in a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 3: FilmCraft 16-second travel reel (12.5 s export)
&lt;/h2&gt;

&lt;p&gt;FilmCraft is a real Premiere clone: source monitor, program monitor, project bin, timeline. I built a 16-second 1080p30 reel from four free Mixkit stock clips, with three cross dissolves and a title.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; one clip was 720p and showed up small in the middle of the 1080p frame. Premiere does the same thing, and one Fill Frame command fixed it.&lt;/p&gt;

&lt;p&gt;The export is the nerdy part. FilmCraft doesn't use FFmpeg; its H.264, HEVC, ProRes and AAC codecs are written from scratch in Rust.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filmcraft-cli --project reel/reel.fcproj export out/travel-reel.mp4 --format h264
"frames": 480, "render_fps": 38.3, "seconds": 12.52, "bytes": 38815510
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;12.5 seconds&lt;/strong&gt; for 480 frames. The file is chunky, though: 38.8 MB for 16 seconds, so compress it before you upload it anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 4: EffectCraft animated title (0.99 s render)
&lt;/h2&gt;

&lt;p&gt;This one surprised me most. You script EffectCraft with After Effects-style JavaScript: comps, layers, keyframes, easing, Gaussian blur. Here's part of the title script I wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addComp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN SOURCE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opacity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;blur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Effects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADBE Gaussian Blur 2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Blurriness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Blurriness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A yellow bar wipes in, the text blurs into place, and a subtitle fades up. The script ran in &lt;strong&gt;0.08 s&lt;/strong&gt;, and the 4-second 1080p title (120 frames) rendered in &lt;strong&gt;0.99 s&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  All four results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PhotoCraft photo edit:&lt;/strong&gt; 0.23 s, layered PSD (hair fringe after background removal)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PhotoCraft batch, 20 photos:&lt;/strong&gt; 1.75 s, 0 failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FilmCraft 16 s 1080p reel:&lt;/strong&gt; 12.5 s export, 38.8 MB file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EffectCraft 4 s title:&lt;/strong&gt; script 0.08 s, render 0.99 s&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real use case: automation
&lt;/h2&gt;

&lt;p&gt;ArtCraft isn't replacing a pro editor tomorrow. The strength is that every app has a command line and an MCP server, so agents like Claude can drive them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make 50 social posts from a spreadsheet&lt;/li&gt;
&lt;li&gt;Fix every product photo in one go&lt;/li&gt;
&lt;li&gt;Turn a script into a rough cut&lt;/li&gt;
&lt;li&gt;Render a title for every video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adobe gives you a beautiful app. ArtCraft gives you an app that scripts and AI agents can use too, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, can you cancel Adobe today?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're a professional with client deadlines: no.&lt;/strong&gt; These apps are days old, labelled alpha, and you will hit missing features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a student, a Linux user, a small business, or a developer who wants to automate creative work: it's already useful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if PhotoCraft keeps moving at 61,000 stars in two weeks, Adobe should be a little nervous.&lt;/p&gt;

&lt;p&gt;ArtCraft apps: &lt;a href="https://getartcraft.com/apps" rel="noopener noreferrer"&gt;https://getartcraft.com/apps&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not sponsored by or affiliated with ArtCraft or Adobe. Tested on a Mac mini M4 (16 GB), October 15, 2026. Price checked on adobe.com the same day. This article was written with AI assistance from my own test logs and video script.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>rust</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>MacBook Pro vs Mac mini M5 Pro for Developers: Where the $800 Gap Really Goes</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:30:46 +0000</pubDate>
      <link>https://dev.to/rainytechlab/macbook-pro-vs-mac-mini-m5-pro-for-developers-where-the-800-gap-really-goes-3051</link>
      <guid>https://dev.to/rainytechlab/macbook-pro-vs-mac-mini-m5-pro-for-developers-where-the-800-gap-really-goes-3051</guid>
      <description>&lt;p&gt;The MacBook Pro with M5 Pro starts at &lt;strong&gt;$2,499&lt;/strong&gt;. The Mac mini with M5 Pro starts at &lt;strong&gt;$1,699&lt;/strong&gt;. Same chip inside, $800 apart. So is the MacBook Pro a rip-off, or is the Mac mini hiding a cost?&lt;/p&gt;

&lt;p&gt;If you write code for a living, here's how to decide.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Fg5YW6Gcx5w" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I haven't benchmarked these Macs myself. Every number here comes from Apple's published specs and Apple's US store prices (before tax) as of Sep 30, 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  First, the fair match
&lt;/h2&gt;

&lt;p&gt;The names get mixed up a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MacBook Pro&lt;/strong&gt; comes with M5, M5 Pro or M5 Max&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac mini&lt;/strong&gt; comes with the new M6, or M5 Pro&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the fair comparison is &lt;strong&gt;M5 Pro vs M5 Pro&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real price gap is $500
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro 14" M5 Pro, 24 GB, &lt;strong&gt;1 TB&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;$2,499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac mini M5 Pro, 24 GB, 512 GB&lt;/td&gt;
&lt;td&gt;$1,699&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac mini M5 Pro, 24 GB, &lt;strong&gt;1 TB&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;$1,999&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Match the storage and the gap is &lt;strong&gt;$500&lt;/strong&gt;, the price of portability.&lt;/p&gt;

&lt;p&gt;But the Mac mini ships with no keyboard, trackpad or screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Magic Keyboard with Touch ID: $149&lt;/li&gt;
&lt;li&gt;Magic Trackpad: $129&lt;/li&gt;
&lt;li&gt;Studio Display: from $1,599&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a Studio Display, the Mac mini setup costs &lt;strong&gt;more&lt;/strong&gt; than the MacBook Pro. So the real question is what you already have on your desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: same chip
&lt;/h2&gt;

&lt;p&gt;Both use the M5 Pro: &lt;strong&gt;15-core CPU, 16-core GPU, 307 GB/s memory bandwidth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For developers, Apple shares one useful number: &lt;strong&gt;Xcode builds 2.1x faster than M1 Pro&lt;/strong&gt;. That was measured on the top M5 Pro with 64 GB, so the base model may be a bit slower. Apple doesn't test these two Macs against each other. With the same chip, everyday builds should feel the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory matters more than cores
&lt;/h2&gt;

&lt;p&gt;Docker, a database, your editor and a browser full of docs all share the same memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both start at &lt;strong&gt;24 GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mac mini: &lt;strong&gt;48 GB&lt;/strong&gt; is +$600&lt;/li&gt;
&lt;li&gt;MacBook Pro: &lt;strong&gt;64 GB&lt;/strong&gt; requires the faster 20-core-GPU chip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why so much? &lt;strong&gt;Local AI models.&lt;/strong&gt; In Apple's test, the top Mac mini M5 Pro reached the first token 22x faster than the original M1 Mac mini. If you want to run models locally, get &lt;strong&gt;48 GB or more&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heat: the real hardware difference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MacBook Pro: the M5 Pro in a body &lt;strong&gt;1.55 cm&lt;/strong&gt; thick&lt;/li&gt;
&lt;li&gt;Mac mini: &lt;strong&gt;5 cm&lt;/strong&gt; tall, with more room for airflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple says the Mac mini makes about &lt;strong&gt;5 dB at idle&lt;/strong&gt;. There's no noise figure for the MacBook Pro, and no published numbers for long, heavy builds on either. In theory the bigger box stays cooler longer. If sustained compile performance matters to you, look for independent sustained-load tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ports and displays
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mac mini:&lt;/strong&gt; up to three displays (each up to 6K), 3x Thunderbolt, HDMI and &lt;strong&gt;Ethernet&lt;/strong&gt; on the back, plus 2x USB-C on the front. A great always-on build box or home server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MacBook Pro:&lt;/strong&gt; three external displays &lt;em&gt;plus&lt;/em&gt; its own screen, 3x Thunderbolt, HDMI, SD card slot, MagSafe, &lt;strong&gt;no Ethernet&lt;/strong&gt;. Close the lid at your desk and it works like a desktop.&lt;/p&gt;

&lt;p&gt;Wireless is a tie: both have Apple's N1 chip with &lt;strong&gt;Wi-Fi 7 and Bluetooth 6&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MacBook Pro 14"&lt;/th&gt;
&lt;th&gt;Mac mini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weight&lt;/td&gt;
&lt;td&gt;1.6 kg&lt;/td&gt;
&lt;td&gt;0.73 kg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battery&lt;/td&gt;
&lt;td&gt;up to 14 h web, 22 h video&lt;/td&gt;
&lt;td&gt;needs an outlet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;screen, keyboard, power&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the extra $500 buys
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;14" XDR display, up to &lt;strong&gt;1,600 nits&lt;/strong&gt; HDR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProMotion&lt;/strong&gt; up to 120 Hz&lt;/li&gt;
&lt;li&gt;12 MP camera, three mics, six speakers&lt;/li&gt;
&lt;li&gt;Keyboard with Touch ID, and a big trackpad&lt;/li&gt;
&lt;li&gt;A battery rated up to 22 hours of video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Buying all that separately costs far more than $500. &lt;strong&gt;The MacBook Pro isn't overpriced. It's a Mac mini with an office built in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; storage can't be upgraded later, so choose it on day one. But spend on &lt;strong&gt;memory first&lt;/strong&gt;; you can always add a fast external drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three builds at three budgets
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~$1,700:&lt;/strong&gt; Mac mini M5 Pro as-is. Perfect if you already own a monitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~$2,200:&lt;/strong&gt; Mac mini with the 18-core chip and 1 TB. The sweet spot for heavy desk work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~$2,500:&lt;/strong&gt; MacBook Pro 14" M5 Pro, with screen, keyboard and battery included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MacBook Pro 14" with the 18-core chip: &lt;strong&gt;$2,699&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;MacBook Pro 16" (18-core included): from &lt;strong&gt;$2,999&lt;/strong&gt;, 2.14 kg, up to 24 h of video&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Under $1,000: Mac mini M6
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$899&lt;/strong&gt; with 16 GB and 256 GB. Its 12-core CPU handles web apps, scripts and school projects well. But memory tops out at &lt;strong&gt;32 GB&lt;/strong&gt;, so for heavy Docker or local AI, the M5 Pro is the safer pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip both&lt;/strong&gt; if you train big models or edit 8K all day. Look at the MacBook Pro with &lt;strong&gt;M5 Max&lt;/strong&gt; (up to 128 GB) instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you buy?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mac mini&lt;/strong&gt; if you work at one desk and already own a good monitor and keyboard. It's the best value and the most memory per dollar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MacBook Pro&lt;/strong&gt; if you move around, travel or take lots of video calls. Everything's in one bag. Starting from nothing, it's actually the cheaper &lt;em&gt;complete&lt;/em&gt; setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My picks:&lt;/strong&gt; with a desk already set up, the Mac mini M5 Pro with 48 GB. For everyone else, the 14" MacBook Pro M5 Pro.&lt;/p&gt;

&lt;p&gt;Same brain inside. You're choosing between a desk and a backpack.&lt;/p&gt;

&lt;p&gt;Full comparison: &lt;a href="https://youtu.be/Fg5YW6Gcx5w" rel="noopener noreferrer"&gt;https://youtu.be/Fg5YW6Gcx5w&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which one would you pick for your dev setup? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance. Specs and prices are from Apple, not my own benchmarks.&lt;/em&gt; I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>mac</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Claude Opus 5 Built a Full-Stack App From One Prompt: 64/64 Tests and 3 Invented Package Versions</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:30:08 +0000</pubDate>
      <link>https://dev.to/rainytechlab/claude-opus-5-built-a-full-stack-app-from-one-prompt-6464-tests-and-3-invented-package-versions-5204</link>
      <guid>https://dev.to/rainytechlab/claude-opus-5-built-a-full-stack-app-from-one-prompt-6464-tests-and-3-invented-package-versions-5204</guid>
      <description>&lt;p&gt;Instead of reading benchmark charts, I gave Claude Opus 5 an empty folder and one prompt: build a full-stack app. I didn't write any of the application code.&lt;/p&gt;

&lt;p&gt;The result: a working project-management dashboard on &lt;strong&gt;Next.js + FastAPI + SQLite + JWT&lt;/strong&gt;, with &lt;strong&gt;64/64 backend tests passing&lt;/strong&gt;. And a few honest gaps I'll show you too.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HMPVmqG1_lI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Opus 5
&lt;/h2&gt;

&lt;p&gt;Anthropic released Opus 5 on July 24, 2026, positioned for complex agentic coding. It's the default model on Claude Max and the strongest on Claude Pro, so it's the one most people are actually using. Anthropic also claims it's much better at &lt;strong&gt;verifying its work and iterating until it succeeds&lt;/strong&gt;. That's a claim I can test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules
&lt;/h2&gt;

&lt;p&gt;The project: &lt;strong&gt;FlowDesk&lt;/strong&gt;, a production-style SaaS project manager.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Next.js + TypeScript, FastAPI + Python, SQLite, JWT auth, a real test suite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules:&lt;/strong&gt;

&lt;ol&gt;
&lt;li&gt;Start from an empty folder (&lt;code&gt;challenge-app&lt;/code&gt;), no template&lt;/li&gt;
&lt;li&gt;One master prompt&lt;/li&gt;
&lt;li&gt;I write no application code&lt;/li&gt;
&lt;li&gt;Claude fixes its own errors&lt;/li&gt;
&lt;li&gt;The app has to genuinely &lt;strong&gt;run&lt;/strong&gt;, not just compile or look good in a screenshot&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It checked the machine first
&lt;/h2&gt;

&lt;p&gt;Before writing code, it wrote the architecture down, then inspected the actual machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ran &lt;code&gt;lsof&lt;/code&gt;, found port 3000 taken, and picked &lt;strong&gt;3001&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;noticed &lt;code&gt;uv&lt;/code&gt; wasn't installed and fell back to the standard &lt;code&gt;venv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;even noted how little disk space was left&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most generated projects assume a clean machine and fall over on a real one. The final structure matched the plan it wrote at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend
&lt;/h2&gt;

&lt;p&gt;Not one giant &lt;code&gt;main.py&lt;/code&gt;. Separate modules for config, database, models, schemas, security and dependencies, with &lt;strong&gt;one router per resource&lt;/strong&gt; (auth, projects, tasks, users, dashboard). &lt;strong&gt;18 endpoints across 11 routes&lt;/strong&gt;, all typed and validated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The most revealing moment: invented versions
&lt;/h3&gt;

&lt;p&gt;It pinned exact dependency versions from memory, and &lt;strong&gt;three didn't exist&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bcrypt 5.0.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email-validator 2.4.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;httpx 0.29.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All plausible, all fictional, all just past the edge of its training data, where a model is least reliable. Its own diagnosis from the build log:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I invented a patch release. The newest bcrypt is 5.0.0, not 5.0.1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It named the failure mode, fixed all three and carried on without being asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson for your own projects:&lt;/strong&gt; check AI-pinned versions against the real package index.&lt;/p&gt;

&lt;p&gt;It also skipped &lt;code&gt;passlib&lt;/code&gt; (the default in most FastAPI tutorials) because passlib reads a bcrypt attribute newer releases removed, and it wrote that reason in a comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontend
&lt;/h2&gt;

&lt;p&gt;Next.js App Router, a shared API client, an auth context holding the token, and a &lt;strong&gt;route group that wraps every authenticated page in a guard&lt;/strong&gt;, so no page has to remember to check. 16 components, with loading, empty and error states.&lt;/p&gt;

&lt;p&gt;The seed data deserves a mention. The demo user is "Avery Chen, Director of Product Operations", and tasks look like "dunning email sequence for failed payments" and "fix IDOR on the export endpoint". A dashboard full of "Project One" reads like a tutorial; this reads like someone's real Tuesday.&lt;/p&gt;

&lt;p&gt;It also checked the responsive layout itself at &lt;strong&gt;1440, 768 and 375 px&lt;/strong&gt;, measuring instead of assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it run?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, first try.&lt;/strong&gt; Uvicorn on port 8000, Next.js ready in 330 ms, no traceback.&lt;/p&gt;

&lt;p&gt;That's a less dramatic story but a better result: the failures had already happened during install and were fixed before anything started. I could have edited those errors in to look like a crash on first run. I didn't.&lt;/p&gt;

&lt;p&gt;Without being told, it called its own health endpoint right after startup and got a &lt;code&gt;200&lt;/code&gt;. That's the difference between finishing and verifying.&lt;/p&gt;

&lt;p&gt;The running app: projects with status, progress and due dates; tasks with priority, assignee and project, all persisted in SQLite. &lt;strong&gt;7 projects, 40 tasks, 35% completion, 3 overdue&lt;/strong&gt;, every number computed by the backend, not hardcoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code review: what holds up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Login doesn't leak accounts:&lt;/strong&gt; an unknown user and a wrong password take the same path and return the same message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent errors:&lt;/strong&gt; every error has the same shape, a code plus a message&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What doesn't
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The token sits in &lt;strong&gt;localStorage&lt;/strong&gt;, so route protection is client-side only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pagination&lt;/strong&gt; anywhere&lt;/li&gt;
&lt;li&gt;The API can edit a task in five ways the UI never exposes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verifying "done"
&lt;/h2&gt;

&lt;p&gt;A model saying it's done and a model being done are different claims.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;64/64 tests pass&lt;/strong&gt;: auth, invalid login, protected endpoints, projects, tasks and profile. I read this off the raw &lt;code&gt;pytest&lt;/code&gt; output, not the model's summary.&lt;/li&gt;
&lt;li&gt;Manual API checks:

&lt;ul&gt;
&lt;li&gt;Protected endpoint, no token → &lt;strong&gt;401&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Forged token → &lt;strong&gt;401&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Correct password → real signed token&lt;/li&gt;
&lt;li&gt;Wrong password → &lt;strong&gt;401&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My scores
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Planning&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code quality&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autonomy&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51/60&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Testing is the weakest: all 64 tests are backend-only, there are &lt;strong&gt;no frontend tests&lt;/strong&gt;, and &lt;strong&gt;no production build was ever run&lt;/strong&gt;. That's the gap a human still has to close before this ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It planned, built, broke, diagnosed itself in plain language and got much further alone than I expected.&lt;/li&gt;
&lt;li&gt;Watch for &lt;strong&gt;invented package versions&lt;/strong&gt; right past the training cutoff.&lt;/li&gt;
&lt;li&gt;"Tests pass" only covers what was tested. Check &lt;em&gt;what&lt;/em&gt; the tests cover.&lt;/li&gt;
&lt;li&gt;Read the raw test output, not the model's summary of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full build, start to finish: &lt;a href="https://youtu.be/HMPVmqG1_lI" rel="noopener noreferrer"&gt;https://youtu.be/HMPVmqG1_lI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What should I ask an AI agent to build next? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance from the recorded build.&lt;/em&gt; I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Claude Code vs Codex vs GitHub Copilot: Same Repo, Same Task, 3 Different Workflows</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:29:30 +0000</pubDate>
      <link>https://dev.to/rainytechlab/claude-code-vs-codex-vs-github-copilot-same-repo-same-task-3-different-workflows-1ikb</link>
      <guid>https://dev.to/rainytechlab/claude-code-vs-codex-vs-github-copilot-same-repo-same-task-3-different-workflows-1ikb</guid>
      <description>&lt;p&gt;Autocomplete suggests the next line. An AI coding &lt;strong&gt;agent&lt;/strong&gt; tries to finish the whole task: it reads your project, edits several files, runs your tests, notices failures and hands the result back for review.&lt;/p&gt;

&lt;p&gt;I gave &lt;strong&gt;Claude Code&lt;/strong&gt;, &lt;strong&gt;OpenAI Codex&lt;/strong&gt; and &lt;strong&gt;GitHub Copilot&lt;/strong&gt; (agent mode) the same FastAPI repository and exactly the same instruction, and watched &lt;em&gt;how&lt;/em&gt; each one works.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/SoP2H4iKpWA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One honest caveat up front: this is a &lt;strong&gt;workflow comparison, not a benchmark&lt;/strong&gt;. Nothing was timed or scored, and all three products change month to month.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What makes something an "agent"?
&lt;/h2&gt;

&lt;p&gt;An agent runs a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a request in plain language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gather context&lt;/strong&gt;: read the files that matter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a plan&lt;/strong&gt;, so you see the intent before anything on disk changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit&lt;/strong&gt; code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run the tests&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;When a test fails, &lt;strong&gt;go back and try again&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step, edit → test → edit again, is the whole difference from autocomplete. And it only acts through the tools and permissions you give it. You still decide what gets accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repo and the challenge
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TaskFlow&lt;/strong&gt; is a small FastAPI service that creates and lists tasks, stored in memory, with existing tests passing.&lt;/p&gt;

&lt;p&gt;The instruction, identical for all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;code&gt;priority&lt;/code&gt; field with three values: &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Let people filter with a query parameter&lt;/li&gt;
&lt;li&gt;Reject anything that isn't one of the three&lt;/li&gt;
&lt;li&gt;Add tests&lt;/li&gt;
&lt;li&gt;Update the README&lt;/li&gt;
&lt;li&gt;Leave everything else alone&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Claude Code: terminal-first
&lt;/h2&gt;

&lt;p&gt;Run in a terminal inside the repo (it also plugs into supported editors):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It looked around first: the file tree, then the three files it actually needed.&lt;/li&gt;
&lt;li&gt;It wrote a &lt;strong&gt;six-step plan&lt;/strong&gt; in plain English. If the plan is wrong, you find out &lt;em&gt;before&lt;/em&gt; a line changes.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;asked permission&lt;/strong&gt; before writing files. Give it the smallest permission that does the job.&lt;/li&gt;
&lt;li&gt;It edited four files and showed the diff.&lt;/li&gt;
&lt;li&gt;Then a test &lt;strong&gt;failed&lt;/strong&gt;: one test expected &lt;code&gt;400&lt;/code&gt;, it got &lt;code&gt;422&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the moment that matters. It read the failure and worked out that &lt;strong&gt;FastAPI validates the enum before the handler runs&lt;/strong&gt;, so an unknown value is rejected with 422, not 400. The framework was right and the test was wrong. It fixed the test, reran the suite, and got &lt;strong&gt;5 passing&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codex: delegate and check in
&lt;/h2&gt;

&lt;p&gt;Here I used Codex's delegated-task shape: describe the task, hand it over, keep working, and check in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A progress view shows where it is: reading, planning, editing, running the suite.&lt;/li&gt;
&lt;li&gt;It hit &lt;strong&gt;the same 400 vs 422 wall&lt;/strong&gt; and resolved it the same way. The codebase decided that answer, not the brand.&lt;/li&gt;
&lt;li&gt;You get two things back: a &lt;strong&gt;summary&lt;/strong&gt; of what it says it did, and a &lt;strong&gt;diff&lt;/strong&gt; of what it actually did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read the diff. The summary is a claim; the diff is the change.&lt;/strong&gt; Delegation is comfortable when checking is cheap: clear scope, green suite, a diff you can read in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Copilot: never leave the editor
&lt;/h2&gt;

&lt;p&gt;Most people know Copilot as grey text one line ahead (Tab to accept). The same product now has chat and an &lt;strong&gt;agent mode&lt;/strong&gt; that edits across files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You pick the context (which files it should consider) and stay in the editor.&lt;/li&gt;
&lt;li&gt;Edits land in place across a working set of four files, each with &lt;strong&gt;keep or undo&lt;/strong&gt;. You approve as you go instead of reading one big diff at the end.&lt;/li&gt;
&lt;li&gt;Tests run in the integrated terminal: &lt;strong&gt;same failure, same fix, same 5 passing tests.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Then the familiar steps: stage, commit, open a pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its pull isn't that it's smarter. It's that it's already where your hands are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed (all three)
&lt;/h2&gt;

&lt;p&gt;Four files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;models:&lt;/strong&gt; an enum, so three loose strings become a type the framework can check (which is why validation was almost free)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;main:&lt;/strong&gt; the route takes an optional, already-validated filter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tests:&lt;/strong&gt; two new cases, one valid value and one that should fail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;README:&lt;/strong&gt; five lines documenting the rule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a small change, and that's the point. The value was never typing those lines. It was holding four files in mind, running tests, reading the failure, and coming back with something reviewable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they differ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lives in&lt;/td&gt;
&lt;td&gt;Terminal (+ editor integrations)&lt;/td&gt;
&lt;td&gt;App, CLI and editor&lt;/td&gt;
&lt;td&gt;Editor and GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You interact by&lt;/td&gt;
&lt;td&gt;Conversation inside your repo&lt;/td&gt;
&lt;td&gt;Assign, check progress, review&lt;/td&gt;
&lt;td&gt;Suggestions, chat, agent mode where you code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Terminal-driven work&lt;/td&gt;
&lt;td&gt;Clearly scoped, testable tasks&lt;/td&gt;
&lt;td&gt;Editor work that ends in a PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your job&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scope, context, tests, read the diff&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the same three times. &lt;strong&gt;Your responsibility doesn't move.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Five habits for working safely
&lt;/h2&gt;

&lt;p&gt;An agent runs commands and reads files, so your boundaries matter. In the video, one reaches for a &lt;code&gt;.env&lt;/code&gt; file. It should stop there, and you should be the reason it stops.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets out of prompts.&lt;/strong&gt; Reference the variable name, never the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant the smallest permission that works.&lt;/strong&gt; Read first; write and run only where needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read commands before approving them.&lt;/strong&gt; Anything that deletes, force-pushes or deploys deserves a pause.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review the diff, not the summary.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run your tests, linter and type checker.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An agent can misunderstand you and be completely confident about it. Confident-and-wrong looks exactly like confident-and-right until you check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the prompt in five parts
&lt;/h2&gt;

&lt;p&gt;Weak: &lt;em&gt;"Make this API better."&lt;/em&gt; Better how? Finished when?&lt;/p&gt;

&lt;p&gt;Strong prompts have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; what "finished" looks like&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; what not to touch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints:&lt;/strong&gt; you decide the edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance tests:&lt;/strong&gt; success is checkable, not arguable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required summary:&lt;/strong&gt; something to review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Goal, scope, constraints, tests, summary. Write those and most problems disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one?
&lt;/h2&gt;

&lt;p&gt;Pick the workflow, not the logo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You live in the command line and want direct control → &lt;strong&gt;terminal-first agent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You'd rather hand off a well-defined task and review the result → &lt;strong&gt;delegation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You want help next to your cursor, ending in a PR → &lt;strong&gt;stay in the editor&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They overlap more every month, so try them on a small, non-sensitive repo and judge the workflow yourself. The skill that lasts is defining the task, giving the right context and verifying the result.&lt;/p&gt;

&lt;p&gt;Full walkthrough: &lt;a href="https://youtu.be/SoP2H4iKpWA" rel="noopener noreferrer"&gt;https://youtu.be/SoP2H4iKpWA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which agent should I test on a full real-world project next? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance from the recorded video.&lt;/em&gt; I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Git Tutorial for Beginners (2026): Commits, Branches, Merge Conflicts and Rebase</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:28:49 +0000</pubDate>
      <link>https://dev.to/rainytechlab/git-tutorial-for-beginners-2026-commits-branches-merge-conflicts-and-rebase-2d6m</link>
      <guid>https://dev.to/rainytechlab/git-tutorial-for-beginners-2026-commits-branches-merge-conflicts-and-rebase-2d6m</guid>
      <description>&lt;p&gt;Picture three developers on one website. One improves login, another changes the cart, and the next morning an older folder has overwritten the newest work. Now there's &lt;code&gt;project-final&lt;/code&gt;, &lt;code&gt;project-final-2&lt;/code&gt; and &lt;code&gt;project-final-really-final&lt;/code&gt;, and nobody knows which copy is right.&lt;/p&gt;

&lt;p&gt;That's the problem Git solves. This tutorial follows one small team building an online shop, &lt;strong&gt;Nova Shop&lt;/strong&gt;, from an empty folder to a release, and every command appears exactly when it solves a real problem.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/S0E5qqDzmLs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What Git is (and what GitHub is)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Git is a distributed version-control system.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version control:&lt;/strong&gt; instead of random backup folders, you save named checkpoints called &lt;strong&gt;commits&lt;/strong&gt;. Each one is a snapshot of the project, with an author, a time and a message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed:&lt;/strong&gt; every clone holds the project &lt;em&gt;and&lt;/em&gt; its full history. Looking at old commits, creating branches and committing all work offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration:&lt;/strong&gt; developers exchange commits and combine their histories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mental model: a &lt;strong&gt;camera&lt;/strong&gt; (save a deliberate snapshot), a &lt;strong&gt;time machine&lt;/strong&gt; (go back to an earlier state) and a &lt;strong&gt;team journal&lt;/strong&gt; (what changed, and why).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub is not Git.&lt;/strong&gt; Git is the software on your computer. GitHub (or GitLab, Bitbucket, a company server) hosts a shared copy and adds pull requests, issues and permissions. &lt;code&gt;git commit&lt;/code&gt; saves locally. Nothing is uploaded until you &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maya&lt;/strong&gt; (product owner) wants login and a shopping cart ready for Friday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alex&lt;/strong&gt; builds login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sam&lt;/strong&gt; builds the cart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both need to change the same app in the same week.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create the repository
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"Alex"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"alex@example.com"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir &lt;/span&gt;nova-shop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nova-shop
git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name and email are authorship info stored in each commit, not a GitHub login. &lt;code&gt;git init -b main&lt;/code&gt; creates a hidden repository and names the first branch &lt;code&gt;main&lt;/code&gt;. It doesn't upload anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git status&lt;/code&gt; is the most useful command in Git.&lt;/strong&gt; When you're not sure what Git expects next, read it before typing anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The four places your work lives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Working directory:&lt;/strong&gt; the files you see and edit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging area:&lt;/strong&gt; a draft of the next commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local repository:&lt;/strong&gt; your saved commits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote repository:&lt;/strong&gt; the shared commits, after you push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alex creates &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;styles.css&lt;/code&gt; and &lt;code&gt;app.js&lt;/code&gt;. He wants the first commit to hold only the page structure and styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add index.html styles.css
git diff &lt;span class="nt"&gt;--staged&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: create storefront shell"&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git add&lt;/code&gt; doesn't save anything permanently. It &lt;em&gt;selects&lt;/em&gt; what goes into the next commit. &lt;code&gt;app.js&lt;/code&gt; stays untracked. That's how one messy work session becomes several clean commits.&lt;/p&gt;

&lt;p&gt;A commit is a full project snapshot that points back to its parent. The message should say what the change does: "Update files" tells your team nothing, "Create storefront shell" does.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Branches: separate work, same history
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;branch is a movable label pointing at a commit&lt;/strong&gt;, not a copy of the folder. That's why branches are cheap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/login
&lt;span class="c"&gt;# build login.html and auth.js&lt;/span&gt;
git add login.html auth.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add customer login form"&lt;/span&gt;
git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"fix: validate empty password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two focused commits: one adds a feature, one fixes a behavior. A reviewer can read them separately, and the team could undo the fix without losing the form. &lt;strong&gt;Main doesn't move.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compare the branch with main:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff main...feature/login   &lt;span class="c"&gt;# changes since the branches split&lt;/span&gt;
git log main..feature/login     &lt;span class="c"&gt;# commits only on the feature branch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile Sam starts &lt;code&gt;feature/cart&lt;/code&gt; from the same main commit and commits "feat: add shopping cart". Neither waits for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Push: share your commits
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin &amp;lt;repository-url&amp;gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;origin&lt;/code&gt; is just a conventional nickname for the remote. &lt;code&gt;-u&lt;/code&gt; remembers the link, so next time &lt;code&gt;git push&lt;/code&gt; is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push only sends commits.&lt;/strong&gt; If &lt;code&gt;auth.js&lt;/code&gt; has uncommitted edits, they stay on your machine. Committing saves; pushing shares.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fetch and pull: get your team's work
&lt;/h2&gt;

&lt;p&gt;Sam's cart got reviewed and merged into the remote main, so Alex's local main is now behind.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git log main..origin/main      &lt;span class="c"&gt;# what arrived?&lt;/span&gt;

git switch main
git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git fetch&lt;/code&gt;&lt;/strong&gt; downloads new commits but doesn't touch your branch or files. It's a safe "what changed?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git pull&lt;/code&gt;&lt;/strong&gt; is fetch + integrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--ff-only&lt;/code&gt;&lt;/strong&gt; refuses to create a surprise merge. If your main and the remote have diverged, it stops and lets you decide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Merge: combine the work
&lt;/h2&gt;

&lt;p&gt;After review (a pull request is a hosting-service feature, not a Git command), the team merges login into main:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; feature/login
git log &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-ff&lt;/code&gt; records a merge commit, the moment the feature joined the product. Some teams prefer fast-forward, squash or rebase. &lt;strong&gt;The rule that matters is: be consistent with your team's workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Merge conflicts aren't errors
&lt;/h2&gt;

&lt;p&gt;Alex changed a button to "Sign in". Sam changed the same line to "Continue". Both are valid, so Git can't guess and stops with conflict markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
&amp;lt;button&amp;gt;Sign in&amp;lt;/button&amp;gt;
=======
&amp;lt;button&amp;gt;Continue&amp;lt;/button&amp;gt;
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/cart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maya picks "Continue to sign in". You edit the file to that final version, remove &lt;strong&gt;every&lt;/strong&gt; marker, run the app and tests, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add login.html
git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolving a conflict is a &lt;em&gt;code decision&lt;/em&gt;, not just deleting markers. And if a merge started at the wrong time: &lt;code&gt;git merge --abort&lt;/code&gt; puts things back.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. checkout vs switch vs restore
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt; does two very different jobs, switching branches and restoring files. Modern Git splits them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main               &lt;span class="c"&gt;# change branch&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/name    &lt;span class="c"&gt;# create + switch&lt;/span&gt;
git switch -                  &lt;span class="c"&gt;# back to the previous branch&lt;/span&gt;

git restore app.js            &lt;span class="c"&gt;# discard unstaged edits (careful!)&lt;/span&gt;
git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; auth.js  &lt;span class="c"&gt;# unstage, keep your edits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;git status&lt;/code&gt; before switching. If switching would overwrite work, Git stops: commit it, stash it, or discard it on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Undo the right way
&lt;/h2&gt;

&lt;p&gt;Pick the command by &lt;strong&gt;where&lt;/strong&gt; the mistake is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unwanted edit, not staged&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git restore &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staged by accident&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git restore --staged &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad commit already pushed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git revert &amp;lt;commit-id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; adds a &lt;em&gt;new&lt;/em&gt; commit that undoes the bad one. History stays intact, and teammates can pull normally.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; moves a branch pointer and can rewrite history. It's fine for local cleanup, but &lt;strong&gt;don't rewrite commits your team already has.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Rebase in plain language
&lt;/h2&gt;

&lt;p&gt;Your private &lt;code&gt;feature/profile&lt;/code&gt; started from an older main, and main has moved on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt; joins the two histories, often with a merge commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebase&lt;/strong&gt; replays your feature commits on top of the newest main, as if you'd started from there.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch feature/profile
git fetch origin
git rebase origin/main
&lt;span class="c"&gt;# on a conflict: fix the file, then&lt;/span&gt;
git add &amp;lt;file&amp;gt;
git rebase &lt;span class="nt"&gt;--continue&lt;/span&gt;
&lt;span class="c"&gt;# or give up safely:&lt;/span&gt;
git rebase &lt;span class="nt"&gt;--abort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebase creates &lt;strong&gt;new commit IDs&lt;/strong&gt;. Great for private branches, risky for shared ones. Neither is "better". Use your team's policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write commits people can use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One logical purpose per commit&lt;/li&gt;
&lt;li&gt;Small enough to review, and tested&lt;/li&gt;
&lt;li&gt;A message that completes "If applied, this commit will…": &lt;strong&gt;"Prevent negative cart quantity"&lt;/strong&gt;, not "fix stuff"&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff --staged&lt;/code&gt; first, so no secrets or temp files slip in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefixes like &lt;code&gt;feat&lt;/code&gt;, &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;docs&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt; help, but consistency matters more than the exact format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The daily workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main
git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/cart-quantity
&lt;span class="c"&gt;# build + test&lt;/span&gt;
git add &amp;lt;files&amp;gt;
git diff &lt;span class="nt"&gt;--staged&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add cart quantity controls"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature/cart-quantity
&lt;span class="c"&gt;# open a pull request, get review, merge&lt;/span&gt;
git switch main
git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt;
git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/cart-quantity
git fetch &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence matters more than any single command: &lt;strong&gt;update, isolate, build, inspect, save, share, review, integrate, clean up.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Git connects project snapshots into a history you can inspect and recover.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;add&lt;/code&gt; picks what goes into a commit, &lt;code&gt;commit&lt;/code&gt; saves it locally, &lt;code&gt;push&lt;/code&gt; shares it.&lt;/li&gt;
&lt;li&gt;Branches are cheap labels, so use one per task.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch&lt;/code&gt; is a safe look, &lt;code&gt;pull --ff-only&lt;/code&gt; is a safe update.&lt;/li&gt;
&lt;li&gt;Conflicts are decisions, not errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restore&lt;/code&gt; for files, &lt;code&gt;revert&lt;/code&gt; for shared commits, and be careful with &lt;code&gt;reset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Merge or rebase: follow your team's rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full 25-minute walkthrough with every command on screen: &lt;a href="https://youtu.be/S0E5qqDzmLs" rel="noopener noreferrer"&gt;https://youtu.be/S0E5qqDzmLs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which Git command confused you most when you started? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance from the video lesson. Every command was re-run on Git 2.50 before publishing.&lt;/em&gt; I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Let OpenAI Codex Build My Website From One Prompt: 5 Minutes 23 Seconds, 12/12 Checks</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:56:56 +0000</pubDate>
      <link>https://dev.to/rainytechlab/i-let-openai-codex-build-my-website-from-one-prompt-5-minutes-23-seconds-1212-checks-48jm</link>
      <guid>https://dev.to/rainytechlab/i-let-openai-codex-build-my-website-from-one-prompt-5-minutes-23-seconds-1212-checks-48jm</guid>
      <description>&lt;p&gt;I gave OpenAI's Codex one prompt, an empty folder and a hard 15-minute limit to build a real personal website for my YouTube channel. It finished in &lt;strong&gt;5 minutes 23 seconds&lt;/strong&gt;. Then I tested it properly: 12 checks, in a real browser, on desktop and on a phone.&lt;/p&gt;

&lt;p&gt;It even hit an error on its own and fixed it without help.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ef5Pf5XGzVc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;No sponsor, and no manual changes to the website's code. This is one recorded run, not a guarantee that every attempt gives the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;Codex got exactly one prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a personal website for my YouTube channel, starting from an empty folder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + TypeScript + Tailwind&lt;/strong&gt;, App Router&lt;/li&gt;
&lt;li&gt;Five sections, in order: &lt;strong&gt;hero, latest videos, skills, about, contact form&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dark mode that &lt;strong&gt;remembers your choice&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A layout that works on a phone, accessible forms, and a build that passes&lt;/li&gt;
&lt;li&gt;My six real video titles, and &lt;strong&gt;use only the facts in the prompt&lt;/strong&gt;, so no invented numbers about me&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I kept it fair
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One prompt, and nobody answers its questions&lt;/li&gt;
&lt;li&gt;A hard stop at 15 minutes&lt;/li&gt;
&lt;li&gt;The 12-point checklist was &lt;strong&gt;locked before&lt;/strong&gt; Codex started&lt;/li&gt;
&lt;li&gt;The test script was locked &lt;strong&gt;one minute in&lt;/strong&gt;, before I looked at any output, with a SHA-256 fingerprint of each file saved, so nobody (me or Codex) could change the test after seeing the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; Codex CLI 0.147.0 · GPT-5.6-Sol · medium effort · standard speed (fast mode off) · my normal ChatGPT plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching Codex work (real timestamps)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0:13&lt;/strong&gt;: makes a plan. Its first idea is to &lt;em&gt;generate an image&lt;/em&gt; for the hero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0:24&lt;/strong&gt;: notices I'd switched image generation off, and &lt;strong&gt;changes plan without asking&lt;/strong&gt;: it will draw the artwork with code instead. It didn't get stuck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0:29–0:36&lt;/strong&gt;: checks the Node version and the newest Next.js (&lt;strong&gt;16.3.5&lt;/strong&gt;) and Tailwind (&lt;strong&gt;4.3.3&lt;/strong&gt;) on npm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0:36–3:40&lt;/strong&gt;: nothing in the log for over three minutes while it thinks and writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3:40&lt;/strong&gt;: &lt;strong&gt;eleven files appear at once&lt;/strong&gt;: page, styles, layout, header, contact form and config. It never ran &lt;code&gt;create-next-app&lt;/code&gt;. It wrote &lt;code&gt;package.json&lt;/code&gt; and every config file by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3:57&lt;/strong&gt;: starts checking its own work: install, build, lint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4:37&lt;/strong&gt;: &lt;strong&gt;exit code 1.&lt;/strong&gt; The build passed, but lint failed with one error in the header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4:53&lt;/strong&gt;: reads the error and edits four files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5:07&lt;/strong&gt;: build and lint again: &lt;strong&gt;exit code 0.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5:23&lt;/strong&gt;: writes its summary and stops, with nine and a half minutes to spare.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Totals: &lt;strong&gt;12 files, about 500 lines of code.&lt;/strong&gt; It read about &lt;strong&gt;560K tokens (93% from cache)&lt;/strong&gt; and wrote about &lt;strong&gt;13K&lt;/strong&gt;. It cost nothing extra beyond my ChatGPT plan.&lt;/p&gt;

&lt;p&gt;Something I liked: my prompt only asked for the &lt;em&gt;build&lt;/em&gt; to pass. &lt;strong&gt;Running lint was Codex's own idea.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12-point test
&lt;/h2&gt;

&lt;p&gt;In a real browser, using the production build, untouched:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Production build passes ✅&lt;/li&gt;
&lt;li&gt;Zero console errors ✅&lt;/li&gt;
&lt;li&gt;All five sections exist, in order ✅&lt;/li&gt;
&lt;li&gt;Navigation links to all five and stays visible on scroll ✅&lt;/li&gt;
&lt;li&gt;Name, tagline and a Watch on YouTube button to my channel ✅&lt;/li&gt;
&lt;li&gt;All six video titles with their lengths, spelled exactly (even the long ones with special characters) ✅&lt;/li&gt;
&lt;li&gt;Every video card links to the right page ✅&lt;/li&gt;
&lt;li&gt;All seven skills ✅&lt;/li&gt;
&lt;li&gt;Empty form → an error under each field ✅&lt;/li&gt;
&lt;li&gt;An email without ".com" → "please enter a valid email", then a valid submit → a success message ✅&lt;/li&gt;
&lt;li&gt;Dark mode survives a page reload ✅&lt;/li&gt;
&lt;li&gt;At phone width, no sideways scrolling, and every section is visible ✅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Final score: 12/12.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Being honest about the score
&lt;/h3&gt;

&lt;p&gt;The first time I ran my test script, it said &lt;strong&gt;10/12&lt;/strong&gt;. Both failures were &lt;em&gt;my test's&lt;/em&gt; fault: the script looked for headings literally named "Skills" and "About", but Codex wrote creative headings like "Tools I build with". The checklist only requires the sections to exist in order, and they do. I fixed the script, logged every change and why, and kept the original 10/12 result in the logs. I never touched the website's code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the code: 3 ideas worth stealing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Dark mode with no white flash
&lt;/h3&gt;

&lt;p&gt;A tiny script in the layout runs &lt;strong&gt;before the page appears&lt;/strong&gt; and reads the saved theme. The idea looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;If you saved a choice, use it. If not, follow your system setting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;try/catch&lt;/code&gt;, because some browsers block storage (private mode, for example). Then the site shows the default theme instead of crashing.&lt;/li&gt;
&lt;li&gt;It's injected with &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. The name sounds scary, but it's safe here because the script is fixed and never comes from a visitor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The lint error it fixed: "You Might Not Need an Effect"
&lt;/h3&gt;

&lt;p&gt;The first header copied the theme into React state inside a &lt;code&gt;useEffect&lt;/code&gt;. Lint flagged it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Calling setState synchronously inside an effect can trigger cascading renders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That causes an extra render right after the first one. The React docs have a whole page on this, &lt;em&gt;You Might Not Need an Effect&lt;/em&gt;: if you can read a value directly, don't copy it into state in an effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex's fix was to delete the state completely.&lt;/strong&gt; The toggle just flips a &lt;code&gt;dark&lt;/code&gt; class on the page and saves the choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSS decides whether to show the sun or the moon icon. That &lt;code&gt;localStorage&lt;/code&gt; line is exactly why dark mode survived the reload in check 11.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A friendly contact form
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The email is checked with a simple pattern: text, &lt;code&gt;@&lt;/code&gt;, text, &lt;code&gt;.&lt;/code&gt;, text. That's what caught my missing ".com".&lt;/li&gt;
&lt;li&gt;All errors are collected into one object and shown together, so you see every mistake at once.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noValidate&lt;/code&gt; on the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; turns off the browser's own popup bubbles, so the site shows clear messages under each field.&lt;/li&gt;
&lt;li&gt;After a valid submit, the form clears, so nobody sends the same message twice by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Small details I liked
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The phone menu button has &lt;strong&gt;&lt;code&gt;aria-expanded&lt;/code&gt;&lt;/strong&gt;, so screen readers announce whether the menu is open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One list of menu links&lt;/strong&gt; feeds both the desktop and phone menus, so you change it in one place.&lt;/li&gt;
&lt;li&gt;Tapping a phone menu link &lt;strong&gt;closes the menu&lt;/strong&gt;, so it doesn't cover the section you jumped to.&lt;/li&gt;
&lt;li&gt;With &lt;strong&gt;reduced motion&lt;/strong&gt; turned on in your OS settings, the smooth scrolling and animations switch off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to write a prompt like this
&lt;/h2&gt;

&lt;p&gt;Five things made the difference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name the tools.&lt;/strong&gt; Next.js, TypeScript, Tailwind, App Router. If you don't, the AI picks for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List the sections in order.&lt;/strong&gt; Codex built exactly those five, with a menu link for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give real content.&lt;/strong&gt; I pasted my real video titles and said "use only this information", which is why nothing about me was invented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make every requirement testable.&lt;/strong&gt; Not "looks good on mobile" but "no sideways scrolling at 375 px wide".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set limits.&lt;/strong&gt; I said don't use port 3000 and don't leave a server running. It followed both and suggested port 4173 instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus: &lt;strong&gt;ask it to check its own work&lt;/strong&gt; with a build, lint and tests. Codex ran lint here on its own, but asking makes sure it happens.&lt;/p&gt;

&lt;p&gt;Try it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @openai/codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in with your ChatGPT account, open an empty folder, and paste your prompt. Results vary by plan, version and settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; 5 minutes 23 seconds, 12/12 checks, it fixed its own lint error, and it didn't invent anything about me. There are no fake subscriber counts and no made-up awards. For a personal site, that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The not-so-good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I asked for Tailwind.&lt;/strong&gt; It installed and imported Tailwind but styled everything with its own CSS classes. That works, but it's disappointing if you wanted to learn Tailwind from the code. My checklist didn't test it, so it isn't in the score.&lt;/li&gt;
&lt;li&gt;The form doesn't actually send anything, because I asked for no backend. To go live you'd connect an email service.&lt;/li&gt;
&lt;li&gt;There's no photo of me, just code-drawn art. That's fine for a demo, but a real portfolio needs your face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Is it worth it?&lt;/strong&gt; For a first version of a personal website, absolutely. But you still need to understand the code, to fix what it gets wrong and to add your own content. If you're a beginner, let it build, then read every file and ask why each line is there. If you already code, it saves you the boring setup, but you're still the reviewer. &lt;strong&gt;It's your name on the website, not the AI's.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full recorded run with the logs, code and browser tests: &lt;a href="https://youtu.be/Ef5Pf5XGzVc" rel="noopener noreferrer"&gt;https://youtu.be/Ef5Pf5XGzVc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Should I give Claude Code the exact same prompt and compare the two sites side by side? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; React docs, &lt;em&gt;You Might Not Need an Effect&lt;/em&gt; (react.dev/learn/you-might-not-need-an-effect) · OpenAI Codex CLI (github.com/openai/codex)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance. The code and numbers come from the recorded run. I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Flask JWT Authentication: Build a Secure Banking API in Python (Login, Roles, Tested)</title>
      <dc:creator>RainyTech</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:55:31 +0000</pubDate>
      <link>https://dev.to/rainytechlab/flask-jwt-authentication-build-a-secure-banking-api-in-python-login-roles-tested-4fja</link>
      <guid>https://dev.to/rainytechlab/flask-jwt-authentication-build-a-secure-banking-api-in-python-login-roles-tested-4fja</guid>
      <description>&lt;p&gt;Most JWT tutorials stop at "here's your token". But a token alone doesn't protect anything. The real security bugs happen one step later, when a logged-in customer can read &lt;em&gt;someone else's&lt;/em&gt; bank account by changing an ID.&lt;/p&gt;

&lt;p&gt;In this tutorial we build a small &lt;strong&gt;banking REST API&lt;/strong&gt; with Flask, Flask-JWT-Extended and SQLite, and we test every success and failure path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;secure password hashing&lt;/li&gt;
&lt;li&gt;login that returns a JWT access token&lt;/li&gt;
&lt;li&gt;protected routes with a Bearer token&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;customer data isolation&lt;/strong&gt;: you only see your own accounts&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;admin-only&lt;/strong&gt; endpoint&lt;/li&gt;
&lt;li&gt;Postman tests plus 7 automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qkIx4dWxm5w" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Project page: &lt;a href="https://github.com/rainyreaksmey/python-jwt-authentication-api" rel="noopener noreferrer"&gt;https://github.com/rainyreaksmey/python-jwt-authentication-api&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code below is simplified to show each idea clearly. The video builds every line on screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The endpoints
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/health&lt;/code&gt;: public&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/auth/login&lt;/code&gt;: public&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/me&lt;/code&gt;: JWT&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/accounts&lt;/code&gt;: JWT&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/transactions&lt;/code&gt;: JWT&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/admin/users&lt;/code&gt;: admin JWT&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Setup and configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt   &lt;span class="c"&gt;# Flask, Flask-JWT-Extended, python-dotenv&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file holds a long JWT secret, a &lt;strong&gt;30-minute token lifetime&lt;/strong&gt;, and the SQLite path. Never commit a real production secret to Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The database: never store passwords, never store money as floats
&lt;/h2&gt;

&lt;p&gt;Three tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;users&lt;/strong&gt;: unique username, &lt;strong&gt;password hash&lt;/strong&gt;, full name, and a role limited to &lt;code&gt;customer&lt;/code&gt; or &lt;code&gt;admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;accounts&lt;/strong&gt;: each belongs to one user through &lt;code&gt;owner_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transactions&lt;/strong&gt;: each belongs to one account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two design choices matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The password itself is never saved&lt;/strong&gt;, only its hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money is stored as integer cents.&lt;/strong&gt; &lt;code&gt;0.1 + 0.2&lt;/code&gt; is not &lt;code&gt;0.3&lt;/code&gt; in floating point, and you don't want that in a bank balance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every query uses &lt;strong&gt;parameterized SQL&lt;/strong&gt; (&lt;code&gt;?&lt;/code&gt; placeholders), so values never get mixed into the query text. That's what stops SQL injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Password hashing with Werkzeug
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;werkzeug.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;generate_password_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_password_hash&lt;/span&gt;

&lt;span class="n"&gt;hash_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_password_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RainyDemo123!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# stored in the database
&lt;/span&gt;&lt;span class="nf"&gt;check_password_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;submitted_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# True or False at login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole password flow. Seeding stores only the hash. Login compares safely with &lt;code&gt;check_password_hash&lt;/code&gt;. We never decrypt or return a stored password, because a hash can't be decrypted.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The login endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/auth/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username and password are required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE username = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check_password_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid username or password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;

    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;additional_claims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the security detail: &lt;strong&gt;a wrong username and a wrong password return the exact same response.&lt;/strong&gt; If they returned different errors, an attacker could find out which usernames exist.&lt;/p&gt;

&lt;p&gt;The token's identity is the user ID, and the role goes in as a claim. The response never includes the password hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Protected routes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@jwt_required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;me&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_jwt_identity&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@jwt_required()&lt;/code&gt; rejects missing, malformed or expired tokens &lt;strong&gt;before&lt;/strong&gt; your function runs. The client sends the token as a header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Bearer &amp;lt;access_token&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Authorization: the part most tutorials skip
&lt;/h2&gt;

&lt;p&gt;Being logged in doesn't mean you can see everything. For accounts, admins keep the base query, but customers get an extra condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM accounts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;params&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; WHERE owner_id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The authorization happens inside the database query, on the server.&lt;/strong&gt; So the customer Rainy can't get Sokha's accounts by changing anything in the client. The same owner condition is applied to transactions, combined with an optional &lt;code&gt;account_number&lt;/code&gt; filter and a &lt;code&gt;limit&lt;/code&gt; that must be between 1 and 100, newest first.&lt;/p&gt;

&lt;p&gt;The admin endpoint checks the stored role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Administrator access required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Real test results (Postman)
&lt;/h2&gt;

&lt;p&gt;Every request below was run on screen against the local server on port 5000:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/health&lt;/code&gt;, no token → &lt;strong&gt;200&lt;/strong&gt;, &lt;code&gt;status: ok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Login with the wrong password → &lt;strong&gt;401&lt;/strong&gt; invalid credentials, no token, and nothing reveals whether the username exists&lt;/li&gt;
&lt;li&gt;Login with the correct password → &lt;strong&gt;200&lt;/strong&gt;, a signed token, &lt;code&gt;Bearer&lt;/code&gt;, &lt;strong&gt;1,800-second&lt;/strong&gt; lifetime, and Rainy's customer profile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/me&lt;/code&gt; with the token → &lt;strong&gt;200&lt;/strong&gt;, Rainy with the customer role&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/accounts&lt;/code&gt; → &lt;strong&gt;200&lt;/strong&gt;, only Rainy's everyday and savings accounts. &lt;strong&gt;Sokha's account is excluded&lt;/strong&gt;, even though it's in the same database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/transactions?limit=…&lt;/code&gt; → &lt;strong&gt;200&lt;/strong&gt;, only Rainy's transactions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/accounts&lt;/code&gt; with &lt;strong&gt;no&lt;/strong&gt; token → &lt;strong&gt;401&lt;/strong&gt;, and the protected data is never queried&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/admin/users&lt;/code&gt; with Rainy's valid &lt;strong&gt;customer&lt;/strong&gt; token → &lt;strong&gt;403&lt;/strong&gt;. The token is valid, but the role isn't.&lt;/li&gt;
&lt;li&gt;Admin login, then &lt;code&gt;GET /api/admin/users&lt;/code&gt; → &lt;strong&gt;200&lt;/strong&gt;, all three users, still with no password hashes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 401 vs 403 difference is the whole lesson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;401 Unauthorized&lt;/strong&gt;: we don't know who you are (no token, or a bad one).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403 Forbidden&lt;/strong&gt;: we know exactly who you are, and you're not allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Automated tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All 7 tests pass&lt;/strong&gt;: health check, successful login, wrong-password rejection, missing-token rejection, customer data isolation, customer forbidden access, and admin access. The tests use a temporary SQLite database and a separate JWT secret, so they never touch your tutorial data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you ship this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace the JWT secret and every demo credential.&lt;/li&gt;
&lt;li&gt;Use HTTPS.&lt;/li&gt;
&lt;li&gt;Keep access tokens short-lived, and add refresh-token revocation when you need it.&lt;/li&gt;
&lt;li&gt;Run behind a production server, not Flask's development server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check authorization for every protected resource on the server, not only in the UI.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hash passwords (Werkzeug) and store money as integer cents.&lt;/li&gt;
&lt;li&gt;Use parameterized SQL everywhere.&lt;/li&gt;
&lt;li&gt;Login returns the same error for a bad username or a bad password.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@jwt_required()&lt;/code&gt; handles &lt;em&gt;authentication&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Owner conditions in your SQL and role checks handle &lt;em&gt;authorization&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Test the failure paths, not just the happy path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full video with every request tested in Postman: &lt;a href="https://youtu.be/qkIx4dWxm5w" rel="noopener noreferrer"&gt;https://youtu.be/qkIx4dWxm5w&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Using FastAPI instead? I built the same idea in one 118-line file here:&lt;/em&gt; &lt;a href="https://dev.to/rainytechlab/fastapi-jwt-authentication-in-2026-a-complete-login-api-in-one-file-argon2-pyjwt-tested-1ii6"&gt;https://dev.to/rainytechlab/fastapi-jwt-authentication-in-2026-a-complete-login-api-in-one-file-argon2-pyjwt-tested-1ii6&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance. The code and numbers come from the recorded run. I make AI, coding and tech tutorials on &lt;a href="https://www.youtube.com/@RainyTechLab" rel="noopener noreferrer"&gt;RainyTech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>security</category>
      <category>api</category>
    </item>
  </channel>
</rss>
