<?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: Kuba Brzeziński</title>
    <description>The latest articles on DEV Community by Kuba Brzeziński (@kuba_brzezinski_2d3356a2b).</description>
    <link>https://dev.to/kuba_brzezinski_2d3356a2b</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%2F3950618%2Fda2f57e3-8acf-4ae6-9442-5d524ce9b90c.jpg</url>
      <title>DEV Community: Kuba Brzeziński</title>
      <link>https://dev.to/kuba_brzezinski_2d3356a2b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kuba_brzezinski_2d3356a2b"/>
    <language>en</language>
    <item>
      <title>I built a Solana memecoin trading bot with Claude AI - here's what actually happened</title>
      <dc:creator>Kuba Brzeziński</dc:creator>
      <pubDate>Sun, 07 Jun 2026 08:18:04 +0000</pubDate>
      <link>https://dev.to/kuba_brzezinski_2d3356a2b/i-built-a-solana-memecoin-trading-bot-with-claude-ai-heres-what-actually-happened-5dl6</link>
      <guid>https://dev.to/kuba_brzezinski_2d3356a2b/i-built-a-solana-memecoin-trading-bot-with-claude-ai-heres-what-actually-happened-5dl6</guid>
      <description>&lt;p&gt;About a month ago I got tired of manually watching crypto charts. Trading felt like a full-time job - stressful, emotional, and impossible to scale. So I did what any developer would do: I decided to automate it.&lt;/p&gt;

&lt;p&gt;But I didn't want to actually trade. I wanted to &lt;em&gt;understand&lt;/em&gt; the market first. So I built a monitoring bot - one that watches new token launches on Pump.fun, runs them through Claude AI for scam detection, and tracks what would have happened if I'd bought.&lt;/p&gt;

&lt;p&gt;No real money on the line. Just data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;The idea was simple in theory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Listen for new tokens on Pump.fun via WebSocket&lt;/li&gt;
&lt;li&gt;Pre-filter obvious scams (creator holds &amp;gt; 50%, liquidity &amp;lt; $5k, under 30 seconds old)&lt;/li&gt;
&lt;li&gt;Send survivors to Claude Haiku with token metadata&lt;/li&gt;
&lt;li&gt;Claude decides: buy or skip, with a confidence score&lt;/li&gt;
&lt;li&gt;Track price via Helius WebSocket, simulate TP/SL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TypeScript, Node.js, Solana web3.js, Jupiter API, and Claude Haiku as the brain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually went wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Pump.fun kept changing their API.&lt;/strong&gt;&lt;br&gt;
This was the most frustrating part. I'd get the WebSocket working, leave it running overnight, come back to zero data. Their endpoint structure changed multiple times during the week I was building. No changelog, no warning. Just silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: WebSocket reconnection logic was a mess.&lt;/strong&gt;&lt;br&gt;
My initial implementation didn't handle disconnects gracefully. When Helius dropped the connection (which happened often on the free tier), the bot just... stopped. Silently. I added reconnection logic but it took longer than it should have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: TP/SL never triggered.&lt;/strong&gt;&lt;br&gt;
The price monitoring was working, but my sell logic had a race condition. By the time the condition was evaluated, the price had already moved past the threshold. Classic async bug - and in memecoin time, a few hundred milliseconds is the difference between profit and loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 4: Latency killed the concept.&lt;/strong&gt;&lt;br&gt;
This is what ultimately paused the project. Helius free tier adds meaningful latency. By the time my bot detected a new token, parsed it, called Claude, got a response, and checked the price - early buyers had already moved the price. The monitoring data was accurate. The timing wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude actually did well
&lt;/h2&gt;

&lt;p&gt;The AI layer was honestly the most reliable part. I fed Claude token metadata - holder distribution, liquidity, creator wallet history, description - and asked for a buy/skip decision with confidence score.&lt;/p&gt;

&lt;p&gt;It caught patterns I hadn't thought to hardcode: celebrity name tokens with no context, descriptions that were copy-pasted from other coins, supply distributions that looked clean on the surface but had obvious accumulation patterns.&lt;/p&gt;

&lt;p&gt;False positives happened, but the pre-filters kept Claude API costs low - roughly $15/month at 500 calls a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;Honest answer: mostly simulated losses, with occasional gems.&lt;/p&gt;

&lt;p&gt;The bot found a few tokens early that pumped 200-300% within an hour. But for every one of those, there were fifteen that went to zero within minutes. The AI filtering helped - it avoided the most obvious scams - but it couldn't predict community momentum, influencer shills, or pure luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I finished up
&lt;/h2&gt;

&lt;p&gt;The core monitoring logic works. What I cleaned up for this challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed the README (including a typo in the repo description)&lt;/li&gt;
&lt;li&gt;Added proper reconnection handling with exponential backoff&lt;/li&gt;
&lt;li&gt;Documented the known latency limitation so anyone who forks this doesn't spend three days figuring out why free-tier RPC kills the strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Infrastructure constraints matter as much as the logic. You can have a perfect algorithm and lose because your data arrives 400ms late.&lt;/p&gt;

&lt;p&gt;Claude Haiku is surprisingly good at pattern recognition on structured data - better than I expected for the cost.&lt;/p&gt;

&lt;p&gt;And Pump.fun's API instability is a real moat. The teams profiting from memecoin trading aren't just smarter - they have paid, reliable infrastructure.&lt;/p&gt;

&lt;p&gt;Would I build it again? Yes. Would I run it with real money on Helius free tier? Absolutely not.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/KubaBrzezinskiDev/trading-monitor-bot" rel="noopener noreferrer"&gt;https://github.com/KubaBrzezinskiDev/trading-monitor-bot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubchallenge</category>
      <category>claude</category>
      <category>typescript</category>
      <category>solana</category>
    </item>
    <item>
      <title>Maven vs Gradle</title>
      <dc:creator>Kuba Brzeziński</dc:creator>
      <pubDate>Fri, 05 Jun 2026 11:37:27 +0000</pubDate>
      <link>https://dev.to/kuba_brzezinski_2d3356a2b/maven-vs-gradle-536b</link>
      <guid>https://dev.to/kuba_brzezinski_2d3356a2b/maven-vs-gradle-536b</guid>
      <description>&lt;h2&gt;
  
  
  How to actually choose and stop overthinking it
&lt;/h2&gt;

&lt;p&gt;Every Java project starts with the same question nobody wants to spend time on: Maven or Gradle?&lt;/p&gt;

&lt;p&gt;And yet somehow people spend hours on it. So let me just tell you how to think about it, what the real differences are, and when each one makes sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  A bit of history so you understand why both exist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ant&lt;/strong&gt; came first. If you have never seen an Ant build file, consider yourself lucky. Pure XML, zero conventions, you had to manually describe every single step of the build. Want to compile? Write a task. Want to copy files? Write a task. Want to run tests? You already know. It worked, but maintaining it was a part-time job. Nobody starts new projects on Ant in 2025. If you encounter it, you are maintaining legacy code and you have my condolences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maven&lt;/strong&gt; came along and said: what if we just agreed on how projects should be structured, and the build tool handled the rest automatically? Revolutionary idea at the time. You declare what you want, Maven figures out how to do it. &lt;code&gt;pom.xml&lt;/code&gt; tells Maven what your project is, what it depends on, and what plugins you need. The rest is convention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gradle&lt;/strong&gt; came later and said: what if we kept the good parts of Maven but replaced XML with an actual programming language? And also made it faster?&lt;/p&gt;

&lt;p&gt;That is basically the whole history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bazel&lt;/strong&gt; also exists, built by Google for building Google-scale monorepos. Genuinely impressive engineering. Also genuinely unnecessary for anything below "we have 50 teams committing to the same repo". If someone suggests Bazel for your three-module Spring Boot app, smile and change the subject.&lt;/p&gt;




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

&lt;p&gt;Maven is convention over configuration taken seriously. Your project follows a standard structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  main/
    java/
    resources/
  test/
    java/
    resources/
pom.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And because Maven knows this structure, it knows how to compile, test, package, and deploy your project without you explaining any of it. The &lt;code&gt;pom.xml&lt;/code&gt; is just a declaration of intent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.3.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. Maven resolves transitive dependencies, downloads what it needs, puts everything in the right place.&lt;/p&gt;

&lt;p&gt;The lifecycle is fixed and predictable: &lt;code&gt;validate&lt;/code&gt; → &lt;code&gt;compile&lt;/code&gt; → &lt;code&gt;test&lt;/code&gt; → &lt;code&gt;package&lt;/code&gt; → &lt;code&gt;verify&lt;/code&gt; → &lt;code&gt;install&lt;/code&gt; → &lt;code&gt;deploy&lt;/code&gt;. You can hook into any phase with a plugin but you cannot reorder them. That is a feature, not a limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The downside:&lt;/strong&gt; XML is verbose. A moderately complex &lt;code&gt;pom.xml&lt;/code&gt; with profiles, plugin configurations, and multi-module setup becomes long fast. It is readable, but long.&lt;/p&gt;




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

&lt;p&gt;Gradle replaces XML with a build script written in either Groovy or Kotlin DSL. The Kotlin DSL (&lt;code&gt;build.gradle.kts&lt;/code&gt;) is the modern choice - you get type safety, autocomplete in IntelliJ, and it looks like actual code because it is actual code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.boot:spring-boot-starter-web:3.3.5"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.boot:spring-boot-starter-test"&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;Cleaner. And because it is a real programming language, you can do things that would require painful plugin setups in Maven. Custom logic, conditional dependencies, dynamic versioning - it is all just code.&lt;/p&gt;

&lt;p&gt;The real advantage though is performance. Gradle has two things Maven does not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental builds&lt;/strong&gt; - Gradle tracks which tasks have inputs that changed. If you only touched one module, Gradle only rebuilds that module. Maven rebuilds everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build cache&lt;/strong&gt; - Gradle can cache task outputs and reuse them across builds, even across machines if you set up a remote cache. On a large multi-module project this is not a small difference. It can be the difference between a 2-minute build and a 15-minute build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The downside:&lt;/strong&gt; because Gradle is flexible, it is also possible to make a mess. A &lt;code&gt;build.gradle.kts&lt;/code&gt; that has accumulated three years of custom logic from five different developers is genuinely hard to understand. Maven's rigidity protects you from this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest comparison
&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;Maven&lt;/th&gt;
&lt;th&gt;Gradle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config format&lt;/td&gt;
&lt;td&gt;XML&lt;/td&gt;
&lt;td&gt;Kotlin / Groovy DSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build speed&lt;/td&gt;
&lt;td&gt;Slower on large projects&lt;/td&gt;
&lt;td&gt;Faster (incremental + cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexibility&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;Depends on who wrote it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE support&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot support&lt;/td&gt;
&lt;td&gt;First class&lt;/td&gt;
&lt;td&gt;First class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-module projects&lt;/td&gt;
&lt;td&gt;Verbose but works&lt;/td&gt;
&lt;td&gt;Cleaner and faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How to actually choose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Maven if:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You are building something where other people need to understand the build quickly. New team members, open source contributors, junior developers. Maven's pom.xml is boring in the best possible way - everyone knows where to look for what. There are no surprises.&lt;/p&gt;

&lt;p&gt;Also pick Maven if your company already uses it everywhere. Consistency in tooling across a team is worth more than marginal build speed improvements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Gradle if:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have a multi-module project and build time is actually bothering you. Gradle's incremental builds make a meaningful difference at scale. If you have a monorepo with 10+ modules and Maven is rebuilding all of them on every change, switching to Gradle will feel like a revelation.&lt;/p&gt;

&lt;p&gt;Also pick Gradle if you want more expressive build scripts. Kotlin DSL is genuinely nicer to write than XML once you are past the initial setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting a new Spring Boot project from scratch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestly it does not matter much. Spring Initializr generates both. Pick Gradle with Kotlin DSL if you want to learn something modern, pick Maven if you want zero friction and maximum familiarity. You can always migrate later - it is annoying but not catastrophic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The migration question
&lt;/h2&gt;

&lt;p&gt;People ask about migrating between them more than you would expect.&lt;/p&gt;

&lt;p&gt;Maven to Gradle is more common and relatively straightforward. Gradle has an &lt;code&gt;init&lt;/code&gt; task that can import a Maven project automatically as a starting point. It will not be perfect but it gets you 80% of the way there.&lt;/p&gt;

&lt;p&gt;Gradle to Maven is rarer and more painful because you are losing flexibility, not gaining it. If you are considering it, you probably had a bad experience with an overly complex Gradle setup, which is a people problem more than a tooling problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Spring Boot ecosystem defaults to
&lt;/h2&gt;

&lt;p&gt;Spring Initializr offers both. The official Spring documentation shows examples in both. Most tutorials online cover both.&lt;/p&gt;

&lt;p&gt;In practice, most enterprise Spring Boot projects I have seen use Maven. Most newer, developer-led projects lean toward Gradle. Neither is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ant&lt;/strong&gt;: do not touch unless forced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maven&lt;/strong&gt;: boring, predictable, everyone understands it, great for teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradle&lt;/strong&gt;: faster on large projects, more expressive, easier to overcomplicate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bazel&lt;/strong&gt;: only if you are Google, or want to feel like Google&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>BookEase - booking plugin for WordPress</title>
      <dc:creator>Kuba Brzeziński</dc:creator>
      <pubDate>Mon, 25 May 2026 12:18:47 +0000</pubDate>
      <link>https://dev.to/kuba_brzezinski_2d3356a2b/bookease-booking-plugin-for-wordpress-437n</link>
      <guid>https://dev.to/kuba_brzezinski_2d3356a2b/bookease-booking-plugin-for-wordpress-437n</guid>
      <description>&lt;h1&gt;
  
  
  I got frustrated with $89 WordPress booking plugins — so I built my own
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;And it actually works. Here's how.&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;If you've ever tried to add appointment booking to a WordPress site, you know the pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bookly&lt;/strong&gt; — $89 one-time, or $9.99/month for the full version. Half the features are locked behind add-ons that cost extra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simply Schedule Appointments&lt;/strong&gt; — free version is crippled. Pro starts at $99/year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amelia&lt;/strong&gt; — $79/year. Nice UI, but massive plugin that loads scripts on every page.&lt;/p&gt;

&lt;p&gt;For a barber, a personal trainer, or a small clinic — this is just too much. They want to accept bookings online. They don't want to pay a monthly subscription forever.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;BookEase&lt;/strong&gt; — a lightweight WordPress booking plugin. One shortcode. No monthly fees. Pay once.&lt;/p&gt;

&lt;p&gt;Here's how I built it, what I learned, and what tripped me up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;I wanted to build something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works with &lt;code&gt;[bookease_form]&lt;/code&gt; on any page&lt;/li&gt;
&lt;li&gt;Guides clients through 4 steps: Service → Date &amp;amp; Time → Details → Confirm&lt;/li&gt;
&lt;li&gt;Sends email confirmations automatically&lt;/li&gt;
&lt;li&gt;Has a clean admin panel to manage bookings&lt;/li&gt;
&lt;li&gt;Is written properly — PHP 8.1+, PSR-4, no jQuery spaghetti&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;I went with the classic WordPress plugin OOP structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bookease/
├── bookease.php              # Main plugin file
├── includes/
│   ├── class-bookease.php    # Core orchestrator
│   ├── class-loader.php      # Action/filter queue
│   ├── class-activator.php   # DB tables on activation
│   └── class-bookease-emails.php
├── admin/
│   ├── class-bookease-admin.php
│   └── partials/             # Dashboard, bookings, services, settings
├── public/
│   ├── class-bookease-public.php
│   ├── js/public.js
│   └── partials/booking-form.php
└── languages/
    └── bookease.pot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;class-loader.php&lt;/code&gt; pattern keeps hooks clean — you queue actions and filters, then run them all at once. No scattered &lt;code&gt;add_action()&lt;/code&gt; calls everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  The database
&lt;/h2&gt;

&lt;p&gt;Three tables, created with &lt;code&gt;dbDelta()&lt;/code&gt; on activation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CREATE TABLE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;bookease_services (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    name varchar(255) NOT NULL,
    duration int(11) NOT NULL DEFAULT 60,
    price decimal(10,2) NOT NULL DEFAULT 0.00,
    description text,
    created_at datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) &lt;/span&gt;&lt;span class="nv"&gt;$charset_collate&lt;/span&gt;&lt;span class="s2"&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;code&gt;dbDelta()&lt;/code&gt; is the WordPress-safe way to create/upgrade tables. It only applies changes — so activating the plugin on an existing install won't destroy data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The slot engine — the tricky part
&lt;/h2&gt;

&lt;p&gt;This is where most booking plugins get it wrong. You can't just divide working hours by service duration.&lt;/p&gt;

&lt;p&gt;You need to account for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Service duration (how long the appointment takes)&lt;/li&gt;
&lt;li&gt;Buffer time between appointments&lt;/li&gt;
&lt;li&gt;Existing bookings (no double-booking)&lt;/li&gt;
&lt;li&gt;Past time slots (don't show slots that already passed today)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_available_slots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$service_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$service&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$service_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$break&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'bookease_break_between'&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="nv"&gt;$hours&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_working_hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$hours&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="nv"&gt;$slots&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nv"&gt;$current&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$hours&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'from'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$end&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$hours&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'to'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$now&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1h buffer for same-day bookings&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$duration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$slot_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$duration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_slot_taken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$slot_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$date&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="nv"&gt;$slots&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'H:i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$current&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$current&lt;/span&gt; &lt;span class="o"&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;$duration&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$break&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$slots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;is_slot_taken()&lt;/code&gt; check queries the DB for any confirmed/pending booking that overlaps the proposed slot. Simple but effective.&lt;/p&gt;




&lt;h2&gt;
  
  
  The frontend — AJAX 4-step form
&lt;/h2&gt;

&lt;p&gt;No page reloads. State lives in a JS object:&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;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;serviceId&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="na"&gt;serviceName&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="na"&gt;date&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="na"&gt;time&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="na"&gt;customerName&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="na"&gt;customerEmail&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="na"&gt;customerPhone&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="na"&gt;notes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step validates before moving to the next. On step 2, picking a date fires an AJAX call to get available slots:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchAvailableSlots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;bookease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ajax_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bookease_get_available_times&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;bookease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;service_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceId&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;renderTimeSlots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slots&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;Nonce verification on every AJAX handler — non-negotiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Email notifications
&lt;/h2&gt;

&lt;p&gt;WordPress's &lt;code&gt;wp_mail()&lt;/code&gt; is fine for most use cases. I built HTML templates with inline CSS (because email clients are a nightmare):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;send_customer_confirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$booking_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$booking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_booking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$booking_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$to&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$booking&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'[%s] Your Booking is Confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bookease_business_name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/html; charset=UTF-8'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_confirmation_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$booking&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;wp_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$headers&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;Compatible with any SMTP plugin (WP Mail SMTP, FluentSMTP) since it hooks into &lt;code&gt;wp_mail&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security checklist
&lt;/h2&gt;

&lt;p&gt;Every form and AJAX handler has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;wp_verify_nonce()&lt;/code&gt; — prevents CSRF&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;sanitize_text_field()&lt;/code&gt; / &lt;code&gt;sanitize_email()&lt;/code&gt; — cleans input&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; — prevents SQL injection&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;current_user_can()&lt;/code&gt; — capability checks in admin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is table stakes for any WordPress plugin. Don't skip it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Add Google Calendar sync from day one.&lt;/strong&gt;&lt;br&gt;
It's the most requested feature by anyone running a service business. I'll add it in v1.1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Better error handling on the frontend.&lt;/strong&gt;&lt;br&gt;
Right now errors are generic. Should be more specific ("That slot was just taken, please pick another").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Consider a REST API instead of admin-ajax.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;admin-ajax.php&lt;/code&gt; works but the WP REST API is cleaner and easier to test.&lt;/p&gt;




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

&lt;p&gt;A working WordPress booking plugin. 4-step form, smart availability engine, email notifications, clean admin panel. PHP 8.1+, PSR-4, ~44KB.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://kuba07.gumroad.com/l/bookease" rel="noopener noreferrer"&gt;kuba07.gumroad.com/l/bookease&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;Building a WordPress plugin from scratch taught me a lot about the ecosystem — hooks, &lt;code&gt;wpdb&lt;/code&gt;, nonces, capability checks. It's a different world from modern frameworks, but the fundamentals (clean architecture, security, separation of concerns) are the same everywhere.&lt;/p&gt;

&lt;p&gt;If you're a developer looking to build passive income, WordPress plugins are still a solid bet. The market is huge, the bar for quality is surprisingly low, and a well-built plugin can sell for years.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #wordpress #php #webdev #sideproject&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>wordpress</category>
      <category>php</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
