<?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: Lars van der Niet</title>
    <description>The latest articles on DEV Community by Lars van der Niet (@larsniet).</description>
    <link>https://dev.to/larsniet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F968447%2Ffe6515d9-8566-4bb8-a261-ece32cce2b6f.png</url>
      <title>DEV Community: Lars van der Niet</title>
      <link>https://dev.to/larsniet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/larsniet"/>
    <language>en</language>
    <item>
      <title>iCare: A free 20-20-20 eye-break reminder for iPhone and Apple Watch</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Wed, 25 Mar 2026 15:13:42 +0000</pubDate>
      <link>https://dev.to/larsniet/icare-a-free-20-20-20-eye-break-reminder-for-iphone-and-apple-watch-56ik</link>
      <guid>https://dev.to/larsniet/icare-a-free-20-20-20-eye-break-reminder-for-iphone-and-apple-watch-56ik</guid>
      <description>&lt;h1&gt;
  
  
  iCare: A free 20-20-20 eye-break reminder for iPhone and Apple Watch
&lt;/h1&gt;

&lt;p&gt;I spend most of my day staring at a screen. That's not a complaint, it's just the job. But after long stretches of focused work I started noticing dry eyes, headaches, and that general foggy feeling by the end of the afternoon. The fix is stupidly simple: every 20 minutes, look at something 20 feet away for 20 seconds. It's called the &lt;a href="https://www.aao.org/eye-health/tips-prevention/computer-usage" rel="noopener noreferrer"&gt;20-20-20 rule&lt;/a&gt; and optometrists have been recommending it for years.&lt;/p&gt;

&lt;p&gt;The problem isn't knowing about the rule. It's actually remembering to do it when you're deep in a problem.&lt;/p&gt;



&lt;h2&gt;
  
  
  Every existing app wanted my money or my patience
&lt;/h2&gt;

&lt;p&gt;I tried a handful of 20-20-20 apps on the App Store. Some were fine but charged a subscription for what is essentially a repeating timer. Others were bloated with gamification, streaks, wellness dashboards, and meditation upsells bolted onto what should be a 20-second break. A few were genuinely abandoned, crashing on launch or stuck on an iOS version from two years ago.&lt;/p&gt;

&lt;p&gt;None of the ones I found supported Apple Watch. That was the real dealbreaker. If I'm deep in code and my phone is on silent across the room, a notification I never see doesn't help. A gentle tap on my wrist actually gets my attention without breaking flow.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;iCare does exactly one thing: remind you to take eye breaks on a schedule, then count you through them.&lt;/p&gt;

&lt;p&gt;The home screen shows a circular timer counting down to your next break. When it fires, you get a notification with three options: start the break, snooze it, or skip it entirely. Tapping "start" opens a 20-second countdown that tells you to look at something in the distance. That's the whole interaction.&lt;/p&gt;

&lt;p&gt;Beyond that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configurable interval&lt;/strong&gt; from 15 to 45 minutes, so you can tune it to how you work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjustable break duration&lt;/strong&gt; between 10 and 30 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule controls&lt;/strong&gt; with active hours and weekdays-only mode, so it stays quiet evenings and weekends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause, snooze, skip&lt;/strong&gt; so you're always in control without disabling the whole thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Today's history&lt;/strong&gt; showing completed and skipped breaks at a glance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apple Watch is what makes it actually useful
&lt;/h2&gt;

&lt;p&gt;The Watch companion was the main reason I built this instead of just setting a recurring timer. Reminders arrive as a haptic tap on your wrist, subtle enough that it doesn't interrupt a meeting or a conversation, but noticeable enough that you won't miss it.&lt;/p&gt;

&lt;p&gt;You can start the countdown directly from the Watch notification. The phone and Watch stay in sync through WatchConnectivity, so starting a break on one device reflects on the other. Settings sync too, so you configure once and both devices stay aligned.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;iCare is a native SwiftUI app targeting iOS 17 and watchOS 10. I wanted it lean, so there are zero third-party Swift packages. Everything is built on Apple frameworks only.&lt;/p&gt;

&lt;p&gt;The architecture is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AppState&lt;/code&gt;&lt;/strong&gt; is the central observable model that owns scheduling, break state, notifications, and Watch sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReminderEngine&lt;/code&gt;&lt;/strong&gt; handles the interval math: next fire dates, active hours, weekday filtering, snooze logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NotificationCoordinator&lt;/code&gt;&lt;/strong&gt; manages &lt;code&gt;UserNotifications&lt;/code&gt; with actionable categories (Start, Snooze, Skip)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WatchSyncManager&lt;/code&gt;&lt;/strong&gt; bridges iPhone and Watch state via &lt;code&gt;WatchConnectivity&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SettingsStore&lt;/code&gt;&lt;/strong&gt; persists preferences in &lt;code&gt;UserDefaults&lt;/code&gt; through a shared App Group so both targets read the same data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a Focus filter integration using App Intents. You can override reminder behavior per Focus mode, so your "Do Not Disturb" or "Sleep" focus can automatically pause reminders without you touching anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iCare/                 iPhone app (SwiftUI)
  App/                 Entry point and root navigation
  Features/            Home, Countdown, History, Settings, Onboarding
  Intents/             Focus filter (SetFocusFilterIntent)

iCareWatch/            watchOS app
  App/                 Entry point
  Features/            Home, Countdown, Notification scene

Shared/                Code shared between iPhone and Watch
  Models/              AppState, settings, break records
  Services/            ReminderEngine, NotificationCoordinator, WatchSyncManager
  DesignSystem/        Colors, typography, shared UI components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project uses XcodeGen for project generation and Fastlane with GitHub Actions for CI/CD. Pushing a version tag triggers the full pipeline: code signing via Match, building via Gym, and uploading to App Store Connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;WatchConnectivity is more finicky than the documentation suggests. The Watch and phone don't always have an active session at the same time, and &lt;code&gt;transferUserInfo&lt;/code&gt; queues messages that can arrive out of order or much later than expected. I ended up using &lt;code&gt;sendMessage&lt;/code&gt; for real-time commands (like "start break now") and &lt;code&gt;updateApplicationContext&lt;/code&gt; for settings sync, which overwrites rather than queues. Getting that split right took some trial and error.&lt;/p&gt;

&lt;p&gt;Local notifications on iOS also have limits I didn't expect. You can only schedule 64 pending notifications at a time, which sounds like a lot until you realize a 20-minute interval across an 8-hour workday burns through 24 per day. The engine pre-schedules a rolling window and refreshes whenever the app comes to the foreground.&lt;/p&gt;

&lt;h2&gt;
  
  
  No accounts, no tracking, no subscription
&lt;/h2&gt;

&lt;p&gt;iCare collects nothing. There's no analytics SDK, no backend, no account creation. Your settings and break history live in UserDefaults on your device and sync to your Watch through Apple's encrypted WatchConnectivity protocol. The privacy policy is short because there's nothing to disclose.&lt;/p&gt;

&lt;p&gt;Charging a subscription for a timer that runs locally on your phone feels wrong. iCare is free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/larsniet/i-care" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>mobile</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>GPT-5.4 dropped. The hype isn't fully justified, but the shift is real</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 11:36:28 +0000</pubDate>
      <link>https://dev.to/larsniet/gpt-54-dropped-the-hype-isnt-fully-justified-but-the-shift-is-real-2303</link>
      <guid>https://dev.to/larsniet/gpt-54-dropped-the-hype-isnt-fully-justified-but-the-shift-is-real-2303</guid>
      <description>&lt;h1&gt;
  
  
  GPT-5.4 dropped. The hype isn't fully justified, but the shift is real
&lt;/h1&gt;

&lt;p&gt;GPT-5.4 came out on March 5, 2026, and within hours my feed was full of people calling it a breakthrough.&lt;/p&gt;

&lt;p&gt;I'm a bit more skeptical.&lt;/p&gt;

&lt;p&gt;The model is better, no doubt. But if you've been paying attention to the last few releases, you'll notice a pattern: the raw logical reasoning doesn't feel dramatically smarter with each version. What does improve (and what I think people are actually responding to without realizing it) is how much better these models get at &lt;em&gt;understanding what you're asking for&lt;/em&gt;. The conversational layer, the intent parsing, the way it doesn't misread your prompt anymore.&lt;/p&gt;

&lt;p&gt;That's useful. But it's not the same as becoming more intelligent.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually changed in GPT-5.4
&lt;/h2&gt;

&lt;p&gt;OpenAI is marketing this one around reasoning and coding improvements. There are also "Thinking" and "Pro" variants for deeper analysis and enterprise workloads.&lt;/p&gt;

&lt;p&gt;The context window is the thing that's actually interesting to me. Reports suggest it's pushing toward one million tokens, which means you can feed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An entire codebase&lt;/li&gt;
&lt;li&gt;A long chain of log files&lt;/li&gt;
&lt;li&gt;Months of documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...and ask questions about all of it in one shot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Full-repo code assistant
├── Ingest entire codebase
├── Understand cross-file dependencies
├── Suggest refactors across modules
└── Answer questions with full context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a gimmick. For developers building AI tooling, larger context genuinely unlocks things that weren't feasible before.&lt;/p&gt;

&lt;p&gt;The agentic workflow improvements are also worth noting. GPT-5.4 is designed to work inside systems that take actions autonomously, not just answer questions. But again, I'd temper expectations here. Better at following multi-step instructions is not the same as actually reasoning through hard problems differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  My honest take on the plateau
&lt;/h2&gt;

&lt;p&gt;I keep seeing people benchmarking these models on math olympiad problems or logic puzzles to prove they're getting smarter. And yes, the scores go up. But when I actually use them day-to-day for real engineering problems, like debugging something subtle, thinking through a design tradeoff, or understanding why a system behaves a certain way, it still falls apart in familiar ways.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;has&lt;/em&gt; noticeably improved across GPT-4, 4.5, and now 5.x is how rarely the model misunderstands your intent. Earlier versions would latch onto the wrong part of your prompt. That happens much less now. The model is better at being a good conversation partner.&lt;/p&gt;

&lt;p&gt;That's a real improvement. I just don't want to confuse it with the model becoming fundamentally better at reasoning.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that does matter for developers
&lt;/h2&gt;

&lt;p&gt;Even if the intelligence plateau is real, the broader shift it's part of is not something to dismiss.&lt;/p&gt;

&lt;p&gt;AI is becoming infrastructure. Not in a hype way, more like the same boring, practical way that databases and message queues became infrastructure. Something you design around, not something you bolt on.&lt;/p&gt;

&lt;p&gt;Applications are increasingly built with an explicit AI layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐
│   Frontend  │
├─────────────┤
│   Backend   │
├─────────────┤
│  Database   │
├─────────────┤
│   AI Layer  │  ← this is just part of the stack now
└─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the patterns that sit in that layer are maturing fast, regardless of which model you plug in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Patterns worth actually learning
&lt;/h2&gt;

&lt;p&gt;If you're going to invest time in this space, focus on the patterns that don't change every time OpenAI ships a new model.&lt;/p&gt;

&lt;h3&gt;
  
  
  RAG (Retrieval-Augmented Generation)
&lt;/h3&gt;

&lt;p&gt;The idea is simple: instead of relying on what the model memorized during training, you pull in relevant data at query time and hand it to the model as context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User query
   ↓
Embed query as vector
   ↓
Search vector database
   ↓
Retrieve relevant documents
   ↓
Pass context + query to model
   ↓
AI generates grounded answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern works with GPT-4, GPT-5.4, Claude, Gemini. The model is almost interchangeable. Getting the retrieval right is the hard part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agents
&lt;/h3&gt;

&lt;p&gt;An agent is just a model that can call tools in a loop until it completes a task. The workflow looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User request
     ↓
AI plans tasks
     ↓
Calls tools (search, code exec, APIs)
     ↓
Writes code
     ↓
Tests code
     ↓
Returns result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether GPT-5.4 is smarter or not, more reliable intent parsing means agents fail less silently. The model is less likely to misinterpret what tool to call or when to stop. That's where the conversational improvements actually translate into something concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugging AI into real pipelines
&lt;/h3&gt;

&lt;p&gt;The most underrated use of these models is as a processing step inside existing infrastructure. Log triage is a good example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are an on-call engineer. Classify this log entry and suggest a root cause.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;logEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data classification, document tagging, summarization. That's where I think AI earns its place in the stack right now. It's not glamorous but it works, and the bar for "good enough" is low enough that even a less-than-perfect model handles it fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  So should you care about GPT-5.4 specifically?
&lt;/h2&gt;

&lt;p&gt;Honestly, probably not that much.&lt;/p&gt;

&lt;p&gt;If you're already building with GPT-4 or similar, the upgrade path is real and the context window improvements are worth testing. But the breakthrough framing in the press and on social media feels overcooked.&lt;/p&gt;

&lt;p&gt;The more interesting question isn't "how smart is this model" but "how is AI changing what I have to build and how I build it." That question has a clear answer: AI is becoming a standard layer in software architecture, and the patterns around it are worth investing in now regardless of which model sits underneath.&lt;/p&gt;

&lt;p&gt;The intelligence plateau might be real. The infrastructure shift definitely is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>news</category>
      <category>openai</category>
    </item>
    <item>
      <title>Developing YoungPWR: Overcoming technical challenges for youth empowerment</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:52:04 +0000</pubDate>
      <link>https://dev.to/larsniet/developing-youngpwr-overcoming-technical-challenges-for-youth-empowerment-76l</link>
      <guid>https://dev.to/larsniet/developing-youngpwr-overcoming-technical-challenges-for-youth-empowerment-76l</guid>
      <description>&lt;p&gt;&lt;a href="/images/youngpwr.webp" class="article-body-image-wrapper"&gt;&lt;img src="/images/youngpwr.webp" alt="Youngpwr team photo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  This is YoungPWR: A platform for youth empowerment
&lt;/h1&gt;

&lt;p&gt;YoungPWR helps you gain more power, happiness, and success. YoungPWR is your coach and provides you with the network you need to be more powerful and happy to be successful and earn just that little bit more. YoungPWR helps you by sharing knowledge through workshops, podcasts, blogs, and social media. And you will find fun work in the area: gigs (clients), a part-time job, internship, or volunteer work. Completely free with a YoungPWR account.&lt;/p&gt;

&lt;h2&gt;
  
  
  My role as a developer
&lt;/h2&gt;

&lt;p&gt;As a developer passionate about impactful solutions, I had the opportunity to develop YoungPWR, a dynamic platform aimed at empowering youth. In this blog post, I will share my journey, the technical challenges I faced, and the solutions I implemented to bring YoungPWR to life. The project was initially started by a group of young web developers that loved the idea of YoungPWR. After some time they did not have the time to continue the project and I took over the project. Almost everything had to be rewritten, because the code was not up to date anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Understanding YoungPWR's mission and vision
&lt;/h3&gt;

&lt;p&gt;YoungPWR is dedicated to empowering youth by providing them with the resources they need to succeed. The platform offers a variety of features, including workshops, podcasts, blogs, and job opportunities, all aimed at helping young people achieve their goals and find success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key features of YoungPWR: Workshops, Podcasts, Blogs, and Job Opportunities
&lt;/h3&gt;

&lt;p&gt;The platform is designed to be a comprehensive resource for youth. Key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workshops&lt;/strong&gt;: Educational sessions on various topics to enhance skills and knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Podcasts&lt;/strong&gt;: Inspirational and informative podcasts featuring experts and successful individuals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blogs&lt;/strong&gt;: Articles and posts covering a wide range of topics relevant to young people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job Opportunities&lt;/strong&gt;: Listings for gigs, part-time jobs, internships, and volunteer work in the local area.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The role of technology in YoungPWR's success
&lt;/h3&gt;

&lt;p&gt;Technology plays a crucial role in delivering the features and functionalities of YoungPWR. The platform leverages modern technologies to ensure a seamless and engaging user experience, enabling youth to access the resources they need easily and efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the tech stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Next.js for the frontend?
&lt;/h3&gt;

&lt;p&gt;For the frontend of YoungPWR, we chose Next.js due to its powerful features and capabilities. Next.js offers server-side rendering (SSR), which improves performance and SEO, making it an ideal choice for our platform. Additionally, Next.js provides a great developer experience with features like automatic code splitting, static site generation, and a robust routing system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Styling with Styled-Components
&lt;/h3&gt;

&lt;p&gt;To style the frontend, we opted for Styled-Components, a CSS-in-JS library. Styled-Components allow us to write CSS directly within our JavaScript, enabling scoped styles and easier component-based styling. This approach enhances maintainability and reusability of styles across the platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend with Strapi: Flexibility and customization
&lt;/h3&gt;

&lt;p&gt;For the backend, we selected Strapi, an open-source headless CMS. Strapi offers great flexibility and customization options, allowing us to create and manage content efficiently. It supports REST and GraphQL APIs, making it easy to integrate with our Next.js frontend. Moreover, Strapi's plugin ecosystem extends its functionality, enabling us to add features as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the infrastructure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hosting on DigitalOcean: Droplets and databases
&lt;/h3&gt;

&lt;p&gt;We chose DigitalOcean for hosting due to its reliability and cost-effectiveness. DigitalOcean Droplets provide scalable virtual private servers, which are perfect for our needs. We set up an Ubuntu droplet to host the Strapi CMS and a PostgreSQL database for data management. This setup ensures a robust and scalable infrastructure for YoungPWR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring PostgreSQL for data management
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is a powerful, open-source relational database system. We chose PostgreSQL for its performance, scalability, and extensive feature set. Setting up PostgreSQL on DigitalOcean was straightforward, and it provides the reliability and efficiency needed to handle our data management tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Strapi on an Ubuntu droplet
&lt;/h3&gt;

&lt;p&gt;Deploying Strapi on an Ubuntu droplet involved several steps. First, we installed the necessary dependencies, including Node.js and NPM. Next, we set up Strapi, configured the connection to our PostgreSQL database, and deployed the CMS. This setup allows us to manage content seamlessly and ensures a stable backend for YoungPWR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ensuring Performance and Security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Implementing NGINX for load balancing and security
&lt;/h3&gt;

&lt;p&gt;To enhance the performance and security of our platform, we implemented NGINX. NGINX acts as a reverse proxy and load balancer, distributing incoming traffic across multiple servers. This setup improves performance by balancing the load and ensures security by acting as an additional layer between users and our backend servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhancing speed and security with Cloudflare
&lt;/h3&gt;

&lt;p&gt;Cloudflare provides a range of services that enhance the speed and security of YoungPWR. By using Cloudflare's CDN, we ensure fast content delivery to users around the globe. Additionally, Cloudflare's security features protect our platform from DDoS attacks and other threats, providing an extra layer of protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying the Next.js Application on Vercel
&lt;/h3&gt;

&lt;p&gt;For the frontend, we chose to deploy our Next.js application on Vercel. Vercel offers seamless integration with Next.js and provides automatic deployments, making it an excellent choice for our platform. Vercel's serverless architecture ensures scalability and high performance, allowing us to deliver a fast and responsive user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overcoming technical challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Integrating Strapi with Next.js
&lt;/h3&gt;

&lt;p&gt;One of the main challenges we faced was integrating Strapi with our Next.js frontend. Strapi provides a robust API, but ensuring smooth communication between the frontend and backend required careful planning and execution. We utilized Strapi's REST and GraphQL APIs to fetch data and dynamically render content on the frontend. Implementing efficient data fetching strategies and handling API responses were key to overcoming this challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Database configuration and management
&lt;/h3&gt;

&lt;p&gt;Configuring and managing the PostgreSQL database presented another set of challenges. Ensuring data integrity, optimizing queries, and managing migrations were critical tasks. We implemented robust database management practices, including regular backups, performance monitoring, and query optimization, to ensure the database performed efficiently and reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: Ensuring seamless User Experience (UX)
&lt;/h3&gt;

&lt;p&gt;Providing a seamless user experience was very important for YoungPWR. This involved optimizing page load times, ensuring responsive design, and creating an intuitive user interface. We leveraged Next.js's server-side rendering and static site generation capabilities to enhance performance. Additionally, we conducted extensive user testing and feedback sessions to refine the user experience and address any usability issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future features and enhancements for YoungPWR
&lt;/h2&gt;

&lt;p&gt;Looking ahead, we have several exciting plans for YoungPWR. Future features include enhanced personalization, a matching platform, AI-driven recommendations, and expanded content offerings. We also plan to integrate more social features, allowing users to connect and collaborate more effectively. The goal is to continuously evolve YoungPWR to provide even more value to the users.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Reflecting on the development journey
&lt;/h3&gt;

&lt;p&gt;Developing YoungPWR has been a rewarding journey filled with challenges and learning experiences. From choosing the right tech stack to overcoming technical hurdles, each step has contributed to creating a robust and impactful platform. The dedication and hard work invested in this project have paid off, resulting in a platform that empowers youth and helps them achieve their goals.&lt;/p&gt;

&lt;h3&gt;
  
  
  The impact of YoungPWR on youth empowerment
&lt;/h3&gt;

&lt;p&gt;YoungPWR has already made a significant impact on youth empowerment by providing valuable resources and opportunities. The platform's workshops, podcasts, blogs, and job listings have helped many young people gain skills, knowledge, and connections. As we continue to grow and improve, we aim to reach even more youth and make a positive difference in their lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts and acknowledgments
&lt;/h3&gt;

&lt;p&gt;In conclusion, the development of YoungPWR has been a great example of the power of technology in driving positive change. I loved working on this project and am proud of what we have accomplished.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>WBW: A Telegram bot that watches web pages for you</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:51:29 +0000</pubDate>
      <link>https://dev.to/larsniet/wbw-a-telegram-bot-that-watches-web-pages-for-you-59m8</link>
      <guid>https://dev.to/larsniet/wbw-a-telegram-bot-that-watches-web-pages-for-you-59m8</guid>
      <description>&lt;h1&gt;
  
  
  WBW: A Telegram bot that watches web pages for you
&lt;/h1&gt;

&lt;p&gt;Some pages don't have notifications. Stock levels, ticket availability, appointment slots — you just have to keep refreshing and hope you catch it. WBW automates that by monitoring a page's content via CSS selectors and sending you a Telegram message the moment the text changes or an element disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;You start a conversation with the bot, send it a URL and one or more CSS selectors, and it begins polling every 60 seconds. When the selected element's text changes, you get a message. When your 12-hour session ends, it tells you that too.&lt;/p&gt;

&lt;p&gt;The monitoring loop runs in two modes depending on the target page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast mode&lt;/strong&gt; — fetches static HTML with Cloudflare bypass via cloudscraper. Lightweight and quick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript mode&lt;/strong&gt; — spins up a headless Chrome instance via Zendriver for pages that load their content dynamically. Slower, but handles anything a real browser can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You set the mode upfront with an environment variable. If a site requires JavaScript execution to render the element you're watching, you enable it globally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The bot is built with Python and runs as a single Docker container. FastAPI serves a health check endpoint on port 8080, and the bot polling loop runs concurrently in the same event loop using asyncio — no threading, no separate processes.&lt;/p&gt;

&lt;p&gt;Sessions are stored in a SQLite database persisted via a Docker volume, so the bot survives restarts without losing active monitors. Up to 5 users can monitor simultaneously; sessions expire automatically after 12 hours.&lt;/p&gt;

&lt;p&gt;The retry logic handles flaky pages gracefully — if an element goes missing, the monitor retries up to three times before notifying you, avoiding false alerts for temporary page states or transient load failures. Network errors get exponential backoff before the bot gives up and reports the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;The whole setup is a single &lt;code&gt;docker-compose up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_token_here
&lt;span class="nv"&gt;USE_JAVASCRIPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;  &lt;span class="c"&gt;# or false for static-only mode&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;docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No other dependencies. The Dockerfile handles Chrome installation for JavaScript mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/larsniet/wbw" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>monitoring</category>
      <category>showdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>S3X Explorer: A VS Code extension for S3-compatible storage</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:50:53 +0000</pubDate>
      <link>https://dev.to/larsniet/s3x-explorer-a-vs-code-extension-for-s3-compatible-storage-5398</link>
      <guid>https://dev.to/larsniet/s3x-explorer-a-vs-code-extension-for-s3-compatible-storage-5398</guid>
      <description>&lt;h1&gt;
  
  
  S3X Explorer: A VS Code extension for S3-compatible storage
&lt;/h1&gt;

&lt;p&gt;If you work with object storage regularly, switching between your editor and a web console to manage files gets old quickly. S3X Explorer brings the file management directly into VS Code — browse buckets, open and edit objects inline, upload and download files, all without leaving the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I use Cloudflare R2 as my primary object storage. It's S3-compatible and cost-effective, but the dashboard is slow for quick operations. I wanted something that felt like a local file explorer — tree navigation, click to open, save to sync — but backed by an S3 API. The existing VS Code extensions I tried either didn't support R2's path-style URL requirement or were abandoned. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;The extension integrates into the VS Code Activity Bar as a tree view that mirrors your bucket structure. From there you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browse&lt;/strong&gt; buckets and folders with pagination for large buckets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open objects&lt;/strong&gt; directly in the VS Code editor — text files open as editable buffers that sync back on save&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; files via right-click or drag and drop, with multipart handling for files over 100MB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download, rename, copy, move, delete&lt;/strong&gt; — the full set of CRUD operations via context menus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk operations&lt;/strong&gt; — multi-select with Ctrl/Cmd+click for batch delete, copy, or move&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate presigned URLs&lt;/strong&gt; with configurable expiry (15 minutes to 7 days), copied straight to clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt; objects by prefix (server-side) or content (client-side)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View object metadata&lt;/strong&gt; — size, content type, storage class, custom headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cloudflare R2 support
&lt;/h2&gt;

&lt;p&gt;R2 requires path-style URLs and works with any region identifier. The extension handles this out of the box with a &lt;code&gt;forcePathStyle&lt;/code&gt; setting. Configuring it takes about 30 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3x.endpointUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;account-id&amp;gt;.auto.r2.cloudflarestorage.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3x.accessKeyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-access-key-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3x.secretAccessKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-secret-access-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3x.forcePathStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3x.region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt;
&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;p&gt;The same configuration works for AWS S3, MinIO, and other S3-compatible services — just swap the endpoint URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;The extension is written in TypeScript and structured around VS Code's extension APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TreeDataProvider&lt;/strong&gt; — drives the bucket/folder/object tree view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FileSystemProvider&lt;/strong&gt; — registers a custom &lt;code&gt;s3x://&lt;/code&gt; URI scheme so VS Code can open and save S3 objects like local files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 client layer&lt;/strong&gt; — wraps the AWS SDK with caching, retry logic, and rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inline editing flow works by mounting a virtual filesystem. When you click an object, VS Code opens it via the &lt;code&gt;s3x://&lt;/code&gt; provider, which fetches the content from S3. On save, the provider pushes the updated content back. To the user it feels identical to editing a local file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Published to two marketplaces
&lt;/h2&gt;

&lt;p&gt;The extension is available on both the VS Code Marketplace and Open VSX (for VS Code-compatible editors like VSCodium). Releases are automated via GitHub Actions — a single workflow publishes to both on every tagged release.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/larsniet/s3x-explorer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Queens: Building a constraint-satisfaction puzzle game</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:50:17 +0000</pubDate>
      <link>https://dev.to/larsniet/queens-building-a-constraint-satisfaction-puzzle-game-5547</link>
      <guid>https://dev.to/larsniet/queens-building-a-constraint-satisfaction-puzzle-game-5547</guid>
      <description>&lt;h1&gt;
  
  
  Queens: Building a constraint-satisfaction puzzle game
&lt;/h1&gt;

&lt;p&gt;The N-Queens problem is a classic: place N queens on an N×N chessboard so no two attack each other. Queens is a variation — instead of chess attack rules, the constraint is simpler and more visual. Crowns can't touch at all, not even diagonally, and the board is divided into colored regions that each need exactly one crown. The result is a puzzle that's easy to understand but requires genuine logical deduction to solve.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Place exactly one crown in each row, column, and color region&lt;/li&gt;
&lt;li&gt;No two crowns can touch, including diagonally&lt;/li&gt;
&lt;li&gt;Tap once to mark a cell as impossible (×), tap again to place a crown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole game. The complexity comes from the interaction between the region constraint and the no-touching rule — eliminating one cell can cascade through the board.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: puzzle generation
&lt;/h2&gt;

&lt;p&gt;Playing the game was the easy part. The hard part was building a generator that consistently produces puzzles that are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uniquely solvable&lt;/strong&gt; — exactly one valid placement of crowns exists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logically deducible&lt;/strong&gt; — no guessing required, every step follows from what's already known&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visually varied&lt;/strong&gt; — different layouts between puzzles, no two feeling the same&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generator follows a five-step pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a valid N-Queens solution with the no-adjacency constraint applied&lt;/li&gt;
&lt;li&gt;Grow colored regions around each queen's position using varied growth strategies (horizontal, vertical, circular)&lt;/li&gt;
&lt;li&gt;Validate the region layout — no mono-color rows or columns, no region dominating more than 40% of the board, all regions orthogonally connected&lt;/li&gt;
&lt;li&gt;Generate initial clues (pre-placed crowns and crosses) that give logical starting points&lt;/li&gt;
&lt;li&gt;Verify uniqueness with a backtracking solver and confirm the DeductionEngine can solve it without guessing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any step fails, the generator retries — up to 100 times — rather than relaxing constraints. The result passes a 49-test validation suite with a 90%+ success rate across grid sizes from 4×4 to 7×7.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DeductionEngine
&lt;/h2&gt;

&lt;p&gt;The solver is what made the "logically deducible" requirement non-trivial. It implements the same reasoning a human player would use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a row has only one cell left in a region, a crown must go there&lt;/li&gt;
&lt;li&gt;If placing a crown in a cell would leave another region with no valid cells, it can be eliminated&lt;/li&gt;
&lt;li&gt;If all remaining valid cells in a region share a row or column, that row or column can be cleared for all other regions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Puzzles are only accepted if the engine can solve them from start to finish using these rules, with no backtracking or guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Built with Next.js and TypeScript. The game state, puzzle generation, and deduction logic all live in &lt;code&gt;src/game/&lt;/code&gt; as pure TypeScript classes — no framework dependencies — making them straightforward to test independently from the UI. The test suite covers all generator requirements and the deduction rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/larsniet/queens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Send passwords, the safe way</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:49:42 +0000</pubDate>
      <link>https://dev.to/larsniet/send-passwords-the-safe-way-5aib</link>
      <guid>https://dev.to/larsniet/send-passwords-the-safe-way-5aib</guid>
      <description>&lt;h1&gt;
  
  
  Paswords, the safe way to share passwords
&lt;/h1&gt;

&lt;p&gt;Paswords is a web-app that enables you to generate and share passwords with whomever you want. The main purpose is to securely share passwords with other people by using a one-time-link system. Simply fill in or generate the password you want to share, set a timer for when the link should be invalidated, and the link will be automatically generated. You can then share the link with anyone you want, and they will be able to see the password you have shared. Once the link is opened, it will be invalidated and can no longer be used. Try it now on paswords.link.&lt;/p&gt;

&lt;p&gt;Moreover, what sets Paswords apart is that it is open-source. This means that the source code is freely accessible to everyone and can be reviewed, contributed to, and modified by the community. It provides transparency, which is essential for a tool focused on security and privacy. The open-source nature of Paswords also enables developers to learn from the code, build upon it, and further improve the platform. It also promotes collaboration and innovation, which is crucial for maintaining the highest security standards. So, while you are using Paswords, you are contributing to a growing community that is dedicated to the security of online data exchange.&lt;/p&gt;

&lt;p&gt;Try it out on &lt;a href="https://paswords.link" rel="noopener noreferrer"&gt;paswords.link&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>security</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to set up a passbolt server on Google Cloud</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:49:06 +0000</pubDate>
      <link>https://dev.to/larsniet/how-to-set-up-a-passbolt-server-on-google-cloud-9k1</link>
      <guid>https://dev.to/larsniet/how-to-set-up-a-passbolt-server-on-google-cloud-9k1</guid>
      <description>&lt;h1&gt;
  
  
  Setup Passbolt with Gmail SMTP on Google Cloud
&lt;/h1&gt;

&lt;p&gt;This guide walks you through setting up a free, self-hosted Passbolt password manager on a Google Cloud VM using Gmail’s SMTP service (with your account &lt;code&gt;mail@email.com&lt;/code&gt;). You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A domain with DNS management.&lt;/li&gt;
&lt;li&gt;A Google Cloud account.&lt;/li&gt;
&lt;li&gt;A Gmail account with an App Password (if you use 2-Step Verification).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 1: Set Up Your Google Cloud VM &amp;amp; DNS
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a Google Cloud Project &amp;amp; Enable Billing&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt; and create a new project.&lt;/li&gt;
&lt;li&gt;Enable billing (a credit card is required, but free tier usage should result in a $0 invoice).&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enable the Compute Engine API&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Search for "Compute Engine API" in the Cloud Console and enable it.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a VM Instance&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt; and click &lt;strong&gt;CREATE INSTANCE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name your instance (e.g., &lt;code&gt;passbolt&lt;/code&gt;), select the &lt;strong&gt;us-central1&lt;/strong&gt; region, and choose the &lt;strong&gt;e2-micro&lt;/strong&gt; machine type.&lt;/li&gt;
&lt;li&gt;Leave other settings at their defaults. Once created, note the VM's &lt;strong&gt;External IP&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Configure Your Domain’s DNS&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Log into your domain registrar (e.g., GoDaddy, Namecheap) and add an A record for your subdomain (e.g., &lt;code&gt;passbolt.yourdomain.com&lt;/code&gt;) pointing to your VM's External IP.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set Up Firewall Rules&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;In Google Cloud, navigate to &lt;strong&gt;VPC network&lt;/strong&gt; → &lt;strong&gt;Firewall rules&lt;/strong&gt; and create a rule to allow inbound HTTP and HTTPS traffic:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;passbolt-ingress&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targets:&lt;/strong&gt; All instances (or use specific tags)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source IP ranges:&lt;/strong&gt; &lt;code&gt;0.0.0.0/0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocols/Ports:&lt;/strong&gt; TCP: &lt;code&gt;80,443&lt;/code&gt;
&amp;gt; &lt;strong&gt;Note:&lt;/strong&gt; Gmail SMTP on port 587 is used only for outbound mail, so no inbound rule is needed for it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 2: Configure Gmail SMTP with Postfix
&lt;/h2&gt;

&lt;p&gt;Passbolt sends emails (for registration, notifications, etc.) via SMTP. Since Google Cloud blocks port 25, we’ll configure Postfix to use Gmail's SMTP server on port 587.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Generate an App Password for Gmail
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Log in to your &lt;code&gt;mail@email.com&lt;/code&gt; Gmail account.&lt;/li&gt;
&lt;li&gt;If 2-Step Verification is enabled, generate an &lt;strong&gt;App Password&lt;/strong&gt; in your Google Account security settings. Save this password for later use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: SSH into Your VM &amp;amp; Switch to Root
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Install Postfix &amp;amp; Required Modules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;postfix libsasl2-modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;When prompted during Postfix’s configuration, choose &lt;strong&gt;Local only&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Edit the Postfix Configuration
&lt;/h3&gt;

&lt;p&gt;Open the Postfix configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add or replace the following lines (remove any SendGrid-specific settings if present):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use Gmail’s SMTP server on port 587
&lt;/span&gt;&lt;span class="n"&gt;relayhost&lt;/span&gt; = [&lt;span class="n"&gt;smtp&lt;/span&gt;.&lt;span class="n"&gt;gmail&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;]:&lt;span class="m"&gt;587&lt;/span&gt;

&lt;span class="c"&gt;# Enable SASL authentication
&lt;/span&gt;&lt;span class="n"&gt;smtp_sasl_auth_enable&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;smtp_sasl_password_maps&lt;/span&gt; = &lt;span class="n"&gt;hash&lt;/span&gt;:/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;postfix&lt;/span&gt;/&lt;span class="n"&gt;sasl_passwd&lt;/span&gt;
&lt;span class="n"&gt;smtp_sasl_security_options&lt;/span&gt; = &lt;span class="n"&gt;noanonymous&lt;/span&gt;

&lt;span class="c"&gt;# Enable TLS encryption
&lt;/span&gt;&lt;span class="n"&gt;smtp_tls_security_level&lt;/span&gt; = &lt;span class="n"&gt;encrypt&lt;/span&gt;
&lt;span class="n"&gt;smtp_tls_CAfile&lt;/span&gt; = /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;ssl&lt;/span&gt;/&lt;span class="n"&gt;certs&lt;/span&gt;/&lt;span class="n"&gt;ca&lt;/span&gt;-&lt;span class="n"&gt;certificates&lt;/span&gt;.&lt;span class="n"&gt;crt&lt;/span&gt;

&lt;span class="c"&gt;# (Optional) Increase header size limit
&lt;/span&gt;&lt;span class="n"&gt;header_size_limit&lt;/span&gt; = &lt;span class="m"&gt;4096000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit (press &lt;strong&gt;Ctrl+X&lt;/strong&gt;, then &lt;strong&gt;Y&lt;/strong&gt;, then &lt;strong&gt;Enter&lt;/strong&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Set Up the SASL Password File
&lt;/h3&gt;

&lt;p&gt;Create or edit the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/postfix/sasl_passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line, replacing &lt;code&gt;YOUR_APP_PASSWORD&lt;/code&gt; with your Gmail App Password (ensure there are no spaces):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[smtp.gmail.com]:587 mail@email.com:YOUR_APP_PASSWORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Secure &amp;amp; Apply the Password File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postmap /etc/postfix/sasl_passwd
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
systemctl restart postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Test the Email Setup (Optional)
&lt;/h3&gt;

&lt;p&gt;Install &lt;code&gt;mailutils&lt;/code&gt; to send a test email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;mailutils
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test email from Postfix using Gmail SMTP"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test Email"&lt;/span&gt; your_email@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your inbox (or spam folder) to confirm receipt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Install Passbolt
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Generate UUIDs for Credentials
&lt;/h3&gt;

&lt;p&gt;Generate three UUIDs to use as secure passwords:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First UUID:&lt;/strong&gt; Database root password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second UUID:&lt;/strong&gt; Passbolt admin database password (&lt;code&gt;passboltadmin&lt;/code&gt; user).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third UUID:&lt;/strong&gt; IT administrator passphrase.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Download and Verify the Passbolt Installer Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://download.passbolt.com/ce/installer/passbolt-repo-setup.ce.sh
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/passbolt/passbolt-dep-scripts/releases/latest/download/passbolt-ce-SHA512SUM.txt
&lt;span class="nb"&gt;sha512sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; passbolt-ce-SHA512SUM.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash ./passbolt-repo-setup.ce.sh &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Bad checksum. Aborting"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; passbolt-repo-setup.ce.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Install the Passbolt CE Server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;passbolt-ce-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Follow the On-Screen Prompts
&lt;/h3&gt;

&lt;p&gt;During installation, use the following guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Proceed with installation?&lt;/strong&gt;&lt;br&gt;
Type: &lt;code&gt;Yes&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Database Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the &lt;strong&gt;root&lt;/strong&gt; database user, enter the &lt;strong&gt;first UUID&lt;/strong&gt; as the password.&lt;/li&gt;
&lt;li&gt;For the Passbolt service user, use &lt;code&gt;passboltadmin&lt;/code&gt; with the &lt;strong&gt;second UUID&lt;/strong&gt; as its password.&lt;/li&gt;
&lt;li&gt;Set the database name to &lt;code&gt;passboltdb&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Web Server (Nginx) Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose to install Nginx: &lt;code&gt;Yes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use auto configuration.&lt;/li&gt;
&lt;li&gt;Enter your Passbolt subdomain (e.g., &lt;code&gt;passbolt.yourdomain.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Provide the email address that owns your Google Cloud project.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;SMTP Options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sender Name:&lt;/strong&gt; e.g., &lt;code&gt;Password Manager&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sender Email:&lt;/strong&gt; &lt;code&gt;mail@email.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP Host:&lt;/strong&gt; &lt;code&gt;smtp.gmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS:&lt;/strong&gt; Yes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;code&gt;587&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Username and password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;mail@email.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; Your Gmail App Password&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Fix File Permissions for JWT Files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-Rf&lt;/span&gt; root:www-data /etc/passbolt/jwt/
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;750 /etc/passbolt/jwt/
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/passbolt/jwt/jwt.key
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/passbolt/jwt/jwt.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Configure Passbolt via the Web Interface
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access the Passbolt UI&lt;/strong&gt;&lt;br&gt;
Open your browser and navigate to your subdomain (e.g., &lt;code&gt;https://passbolt.yourdomain.com&lt;/code&gt;). You should see the Passbolt welcome page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complete the Initial Setup:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Settings:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host:&lt;/strong&gt; &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;code&gt;3306&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User:&lt;/strong&gt; &lt;code&gt;passboltadmin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; Use the &lt;strong&gt;second UUID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Name:&lt;/strong&gt; &lt;code&gt;passboltdb&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;OpenPGP Key Generation:&lt;/strong&gt;
 Follow the prompts to generate your server’s OpenPGP key (set the server name as "passbolt" and use your associated email).&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;SMTP Settings:&lt;/strong&gt;
 Confirm the following:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sender Name:&lt;/strong&gt; e.g., &lt;code&gt;Password Manager&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sender Email:&lt;/strong&gt; &lt;code&gt;mail@email.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP Host:&lt;/strong&gt; &lt;code&gt;smtp.gmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS:&lt;/strong&gt; Enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;code&gt;587&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Username &amp;amp; password (Gmail credentials)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test Email Functionality:&lt;/strong&gt;&lt;br&gt;
Use the Passbolt interface to send a test email. Check your inbox (or spam folder) to verify that the email is received.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create the Administrator Account:&lt;/strong&gt;&lt;br&gt;
Create a new administrator account (e.g., "IT") using the &lt;strong&gt;third UUID&lt;/strong&gt; as the passphrase. Follow any additional on-screen instructions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 5: Final Steps &amp;amp; Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify Web Access:&lt;/strong&gt;&lt;br&gt;
Confirm that you can access Passbolt at your subdomain via HTTPS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test Sending Emails:&lt;/strong&gt;&lt;br&gt;
Use Passbolt’s built-in test email function to ensure Gmail SMTP is working correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review DNS &amp;amp; Firewall Settings:&lt;/strong&gt;&lt;br&gt;
Make sure your domain’s A record points to your VM’s IP and that ports 80 and 443 are open.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secure Your Credentials:&lt;/strong&gt;&lt;br&gt;
Safely store your generated UUIDs and Gmail App Password, as these are critical for managing your Passbolt installation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloud</category>
      <category>google</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Lemonbike: Cycling with a purpose</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:48:30 +0000</pubDate>
      <link>https://dev.to/larsniet/lemonbike-cycling-with-a-purpose-5gcg</link>
      <guid>https://dev.to/larsniet/lemonbike-cycling-with-a-purpose-5gcg</guid>
      <description>&lt;h1&gt;
  
  
  A fresh breeze in the Bollenstreek
&lt;/h1&gt;

&lt;p&gt;I was assigned to make the website clear for both the computer, as well as the tablet and mobile phone. The UX/UI was created by a designer who was hired by Lemonbike to give the website a fresh look. I then converted this design into a web application that, among other things, supports multiple languages, displays all products that Lemonbike rents, is linked to the reservation and payment module, and answers the most frequently asked questions. The reservation and payment module has been efficiently set up together with an external company. It also supports multiple languages, and all reservations can be viewed on a computer in the branches themselves.&lt;/p&gt;

&lt;p&gt;Lemonbike sets itself apart with its social mission. By renting out (and in some cases offering for free) special bicycles, people with physical and/or mental disabilities and their families can enjoy the Bollenstreek. Lemonbike branches can be found in Katwijk, Noordwijk, and Lisse.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Image Renamer: Bringing order to your photo library</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:47:55 +0000</pubDate>
      <link>https://dev.to/larsniet/image-renamer-bringing-order-to-your-photo-library-2kgg</link>
      <guid>https://dev.to/larsniet/image-renamer-bringing-order-to-your-photo-library-2kgg</guid>
      <description>&lt;h1&gt;
  
  
  Image Renamer: Bringing order to your photo library
&lt;/h1&gt;

&lt;p&gt;If you've ever transferred photos from a digital camera or phone, you've likely encountered files named something like &lt;code&gt;IMG_0001.jpg&lt;/code&gt; — and then another batch starting at &lt;code&gt;IMG_0001.jpg&lt;/code&gt; again after reformatting the card. Sorting through thousands of cryptically named files to find a specific photo is a frustrating experience. That's exactly the problem Image Renamer was built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Digital cameras reset their file numbering every time an SD card is formatted. After just a few trips, you can end up with dozens of files sharing the same name, scattered across different folders. Sorting by name becomes useless, and sorting by date only works if your file system preserved the original timestamps — which it often doesn't when copying between devices.&lt;/p&gt;

&lt;p&gt;The actual creation date, however, is almost always preserved inside the file itself as EXIF metadata. Every modern camera embeds the exact date and time a photo was taken directly into the file. Image Renamer reads that data and uses it to give every file a meaningful, chronologically sortable name.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The core of the application is straightforward. For each image file in a folder, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the &lt;code&gt;DateTimeOriginal&lt;/code&gt; field from the file's EXIF metadata using Pillow&lt;/li&gt;
&lt;li&gt;Falls back to the file's system creation timestamp if no EXIF data is present&lt;/li&gt;
&lt;li&gt;Generates a new filename using a configurable &lt;code&gt;strftime&lt;/code&gt; format (default: &lt;code&gt;2023-04-25_14-30-15.jpg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Handles collisions by appending a counter suffix, so no files are ever silently overwritten&lt;/li&gt;
&lt;li&gt;Optionally creates a backup of all original files before renaming&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a folder where every file has a unique, human-readable name that sorts perfectly in any file explorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Supports JPG, JPEG, PNG, NEF, CR2, and ARW image formats&lt;/li&gt;
&lt;li&gt;Optional video support (MP4, MOV, AVI, MKV, and more)&lt;/li&gt;
&lt;li&gt;Customizable filename format using standard Python &lt;code&gt;strftime&lt;/code&gt; codes&lt;/li&gt;
&lt;li&gt;Backup mode to keep originals safe&lt;/li&gt;
&lt;li&gt;Duplicate detection — skip or remove files taken at the exact same timestamp&lt;/li&gt;
&lt;li&gt;Settings persistence, so your preferences are remembered between sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two ways to use it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GUI
&lt;/h3&gt;

&lt;p&gt;For anyone who prefers a visual interface, the app ships with a PyQt6-based GUI. You can select your folder, pick a date format from the presets or define your own, toggle backup and video options, and watch the rename operation complete in real time with a live log.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;

&lt;p&gt;For scripting or bulk operations, the command-line interface covers all the same functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Basic usage&lt;/span&gt;
imagerenamer /path/to/photos

&lt;span class="c"&gt;# With backup&lt;/span&gt;
imagerenamer /path/to/photos &lt;span class="nt"&gt;--backup&lt;/span&gt;

&lt;span class="c"&gt;# Include video files&lt;/span&gt;
imagerenamer /path/to/photos &lt;span class="nt"&gt;--include-videos&lt;/span&gt;

&lt;span class="c"&gt;# Custom filename format&lt;/span&gt;
imagerenamer /path/to/photos &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"%Y%m%d_%H%M%S"&lt;/span&gt;

&lt;span class="c"&gt;# Remove duplicates instead of renaming with a suffix&lt;/span&gt;
imagerenamer /path/to/photos &lt;span class="nt"&gt;--remove-duplicates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Distribution
&lt;/h2&gt;

&lt;p&gt;One of the goals was to make the tool accessible to non-technical users. The project uses GitHub Actions to automatically build platform-native executables on every release — a standalone &lt;code&gt;.exe&lt;/code&gt; for Windows, a &lt;code&gt;.app&lt;/code&gt; bundle for macOS, and a binary for Linux. No Python installation required. Anyone can download it and run it immediately.&lt;/p&gt;

&lt;p&gt;For developers, it's also available on PyPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;modern-image-renamer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;Building cross-platform GUI applications in Python comes with some quirks. PyInstaller, which packages the app into standalone executables, often triggers false positives in antivirus software on Windows — a common frustration for developers distributing Python apps. The solution was to document the issue clearly and provide alternative installation paths for users who encounter it.&lt;/p&gt;

&lt;p&gt;Setting up the CI/CD pipeline with GitHub Actions also taught me a lot about matrix builds — running the same build job in parallel on Windows, macOS, and Linux runners and producing platform-specific artifacts from a single workflow.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/larsniet/image-renamer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt; · &lt;a href="https://github.com/larsniet/image-renamer/releases/latest" rel="noopener noreferrer"&gt;Download latest release&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
    <item>
      <title>A self-updating GitHub profile README</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:47:19 +0000</pubDate>
      <link>https://dev.to/larsniet/a-self-updating-github-profile-readme-54ei</link>
      <guid>https://dev.to/larsniet/a-self-updating-github-profile-readme-54ei</guid>
      <description>&lt;h1&gt;
  
  
  A self-updating GitHub profile README
&lt;/h1&gt;

&lt;p&gt;GitHub has a hidden feature: if you create a repository with the same name as your username, its README is displayed directly on your profile page. It's essentially a personal landing page, but in markdown. The catch is that it gets stale fast — every time you finish a new project, you have to remember to update it manually.&lt;/p&gt;

&lt;p&gt;I didn't want to do that. So I automated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The portfolio website already exposes all my projects through an API endpoint at &lt;code&gt;/api/projects&lt;/code&gt;. Each project has a title, description, URL, and a &lt;code&gt;featured&lt;/code&gt; flag. That's all the data needed to keep the README up to date.&lt;/p&gt;

&lt;p&gt;A GitHub Actions workflow runs on a schedule — every hour — and does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetches&lt;/strong&gt; the latest projects from &lt;code&gt;larsniet.com/api/projects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters&lt;/strong&gt; them using &lt;code&gt;jq&lt;/code&gt;: featured projects go into one list, everything else into another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injects&lt;/strong&gt; the generated content into the README between a pair of HTML comment markers using &lt;code&gt;awk&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commits and pushes&lt;/strong&gt; the result if anything changed
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;workflow_dispatch&lt;/code&gt; trigger is useful for forcing an immediate update after publishing a new post, without waiting for the next scheduled run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The marker system
&lt;/h2&gt;

&lt;p&gt;The README uses HTML comment markers to define the zones that get replaced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- FEATURED-PROJECTS:START --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Project title&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; — description
&lt;span class="c"&gt;&amp;lt;!-- FEATURED-PROJECTS:END --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;awk&lt;/code&gt; script scans the file line by line, and when it hits a &lt;code&gt;START&lt;/code&gt; marker it prints the new content and sets a skip flag. Everything between the markers gets replaced. Lines outside the markers are passed through untouched, so the rest of the README stays exactly as written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Whenever I publish a new post on my portfolio, the GitHub profile updates itself within the hour. Featured projects appear at the top, the rest follow below — all without me touching the profile repository.&lt;/p&gt;

&lt;p&gt;It's a small thing, but it means the profile always reflects the actual current state of my work rather than whatever I last remembered to write down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/larsniet/larsniet" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>github</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Walk local, eat local</title>
      <dc:creator>Lars van der Niet</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:46:43 +0000</pubDate>
      <link>https://dev.to/larsniet/walk-local-eat-local-5a51</link>
      <guid>https://dev.to/larsniet/walk-local-eat-local-5a51</guid>
      <description>&lt;h1&gt;
  
  
  A culinary walk through the region
&lt;/h1&gt;

&lt;p&gt;Dinnerwalks began as a small-scale project to support the hospitality industry, conceived by &lt;a href="https://www.linkedin.com/in/veerle-killan-11a704149/" rel="noopener noreferrer"&gt;Veerle&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/merlijn-killan-091546182/" rel="noopener noreferrer"&gt;Merlijn Killan&lt;/a&gt;. Soon, registrations started pouring in, and the process that used payment requests and manual emails became a significant burden. That's why Merlijn asked me for help. My task was to transform the outdated website into a modern and unique web application that automated the existing process.&lt;/p&gt;

&lt;p&gt;A (dutch) video about the project can be found &lt;a href="https://www.youtube.com/watch?v=YFe9owbPVaE&amp;amp;ab_channel=NoordwijkNu" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
