<?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: Yoram Kornatzky</title>
    <description>The latest articles on DEV Community by Yoram Kornatzky (@kornatzky).</description>
    <link>https://dev.to/kornatzky</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%2F485231%2F3d1690d1-f67c-4d15-bc47-0740e9f5bd59.jpeg</url>
      <title>DEV Community: Yoram Kornatzky</title>
      <link>https://dev.to/kornatzky</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kornatzky"/>
    <language>en</language>
    <item>
      <title>Household Manager: agents that run the house on the clock</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Thu, 15 Oct 2026 18:15:02 +0000</pubDate>
      <link>https://dev.to/kornatzky/household-manager-agents-that-run-the-house-on-the-clock-1el3</link>
      <guid>https://dev.to/kornatzky/household-manager-agents-that-run-the-house-on-the-clock-1el3</guid>
      <description>&lt;p&gt;Household Manager is a Laravel application I am building in public. The point is not a chat window that invents a grocery list. The point is a set of agents that already know the house, already know the pantry, and already fire at 08:00 in the user’s timezone.&lt;/p&gt;

&lt;p&gt;The four original domains are cooking, shopping, maintenance, and cleaning. Those four are now in the app. Along the way I had to make the clock honest: a missed minute is a late run, not a skipped day.&lt;/p&gt;

&lt;p&gt;This article is the long form of the build series I have been posting on LinkedIn and X. Same facts. Room to show how the slices lock together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this project
&lt;/h2&gt;

&lt;p&gt;I wanted a real application for the official Laravel AI SDK, not a demo agent in Tinker. Cursor does the agentic development. The stack stays Laravel and PHP: queues, notifications, Pest, structured output, conversation memory.&lt;/p&gt;

&lt;p&gt;Users instruct the system through a web app. Settings are plain forms. The agents do not wait for a prompt at meal time. A scheduler ticks every minute and calls them when the local clock says so.&lt;/p&gt;

&lt;p&gt;That constraint is the whole design. If the agent only works when you open a chat, it is a recipe generator. If it works when you are not looking, it is a household manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first slice: cooking
&lt;/h2&gt;

&lt;p&gt;Cooking was the first function that actually did something at 08:00.&lt;/p&gt;

&lt;p&gt;Each user has a Cooking Agent. You set breakfast, lunch, and dinner times, how many people, city and country, and dietary notes as plain text. Location is geocoded so cuisine and ingredients fit the place. One of the early test households is two people, Ukrainian lunch, not too much sugar, one person diabetic.&lt;/p&gt;

&lt;p&gt;A scheduler runs every minute. At each meal time in the user’s timezone, a job calls the agent. The agent returns structured dishes: ingredients, cooking instructions, serving instructions, and a generated photo of the plated meal. It remembers the conversation, so the menu varies instead of repeating the same dish three days in a row.&lt;/p&gt;

&lt;p&gt;There is a Cooking settings screen and a Meals calendar. Open a day and you get breakfast, lunch, and dinner.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7ye67g8ew31i1qyu1q8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7ye67g8ew31i1qyu1q8.png" alt="A planned lunch: chilled beet and kefir soup, salad, stuffed peppers. Cuisine Ukrainian, two servings, diabetic note." width="800" height="470"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F24lt71gcj069t5fonnjg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F24lt71gcj069t5fonnjg.png" alt="Cooking settings and the meals calendar." width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Serving a meal is a real event. Stock goes down. Meal times do not invent extra dishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock before the stove
&lt;/h2&gt;

&lt;p&gt;Planning the menu is only half of cooking. You still need to know when to start.&lt;/p&gt;

&lt;p&gt;The Cooking Agent estimates preparation time as structured output. Each meal stores prep in minutes. Dishes run in parallel when that is practical. The same every-minute scheduler that handles serving fires at &lt;code&gt;served_at - prep&lt;/code&gt;. Laravel Notifications sends one mail: start now, this is dinner, here are the dishes. Each meal is notified once.&lt;/p&gt;

&lt;p&gt;A dinner served at 21:00 with about 30 minutes of prep mails at 20:30. A 00:15 meal with 30 minutes of prep notifies at 23:45 the day before. That wrap has to work or the clock is a toy.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6suwl63i27wwurhhl9gh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6suwl63i27wwurhhl9gh.png" alt="The start-cooking mail for tonight’s dinner." width="799" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prep-time mail. The agent owns the clock, not only the recipe.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopping from the pantry, not from a chat
&lt;/h2&gt;

&lt;p&gt;The second piece is a Shopping Agent on the same SDK. It reads the week’s ingredients, subtracts what is already in inventory, and that is the list. Inventory is grouped the way a store is grouped: Produce, Dairy, Meat and fish, and so on. Units follow Metric or US from Cooking settings. Each item gets a generated product photo.&lt;/p&gt;

&lt;p&gt;Mark it purchased and stock goes up.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyph0qvtevqc9855k8i82.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyph0qvtevqc9855k8i82.png" alt="This week’s list, 31 August–6 September 2026. Apples and blueberries still to buy. Tomatoes already in stock." width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The agent only puts on the list what you do not already have.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo37b477pfdxlol6u3of.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo37b477pfdxlol6u3of.png" alt="Produce on the shelf." width="799" height="413"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04f9afazlojkj6rmzu3m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04f9afazlojkj6rmzu3m.png" alt="Inventory category grid." width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A checkbox is the wrong payload. Need 3 kg of tomatoes and the store only has 2.5? Mark 2.5. Remaining 0.5 kg stays on the list for the next stop, not a new list. Need 3 eggs and you grab 5? Stock becomes 5, and the line is done.&lt;/p&gt;

&lt;p&gt;Each card already holds what is left to buy. One click, no typing, means you bought that amount. A different number is a short aisle or an extra pack. Inventory only rises by the amount you enter.&lt;/p&gt;

&lt;p&gt;The trip writes a quantity. That is what I want the agent to see: bought 2.5 kg, 0.5 kg still open, stock +2.5.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0pnuhrlzdbz3ce5381a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0pnuhrlzdbz3ce5381a.png" alt="Tomatoes after 2.5 of 3 kg, with 0.5 still to buy." width="800" height="500"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkf06ixp5v0ztnmg6y2b.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkf06ixp5v0ztnmg6y2b.png" alt="The same week after the last 0.5 kg, and eggs bought as 5 of 3." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A grocery list is a running remainder, not a checkbox.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lead time, counted back from Monday breakfast
&lt;/h2&gt;

&lt;p&gt;Shopping time plus delivery time is counted back from Monday breakfast. A scheduled job, &lt;code&gt;shopping:prepare&lt;/code&gt;, still runs every minute. It fires when now is at or after Monday breakfast minus that lead. A three-day lead starts next week’s list on Friday at 08:00. A nine-day lead skips a Monday that is already too close and lands on the next one that still fits.&lt;/p&gt;

&lt;p&gt;Default is one shopping day and no delivery. You set days and hours on a Shopping times screen. Under the heading, a live line shows when the next list will start preparing.&lt;/p&gt;

&lt;p&gt;One of the screenshots is that screen with 1 shopping day and 2 delivery days. Breakfast is 08:00 from Cooking, so the next list starts Friday, 11 September at 08:00 — three days before Monday breakfast. Groceries have to be in the house before that meal.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jcoez6qg438y4tefqxd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jcoez6qg438y4tefqxd.png" alt="Shopping times: 1 shopping day, 2 delivery days, next list Friday 11 September at 08:00." width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that can kill a mutant
&lt;/h2&gt;

&lt;p&gt;Once cooking and shopping were live, the safety net had to be real. Pest 5 on the Laravel AI SDK build, so a dish, a shopping list, and a pantry change cannot drift without a test noticing.&lt;/p&gt;

&lt;p&gt;What landed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Feature tests cover agents, meals, shopping, inventory, settings, and auth.&lt;/li&gt;
&lt;li&gt;Architecture tests keep the shape of the app honest.&lt;/li&gt;
&lt;li&gt;One Playwright browser test hits the real home page. Trap: if Vite HMR is running, Chromium waits forever on the hot file. Tests now load the built assets instead.&lt;/li&gt;
&lt;li&gt;Evals cover the three application agents. They run only with &lt;code&gt;--evals&lt;/code&gt;. Live-model cases still skip — the model rejects a temperature setting Pest wants to send.&lt;/li&gt;
&lt;li&gt;Mutation testing rewrites the code on purpose and checks the suite still fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Numbers from a fresh local run at the time of that post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Line coverage of &lt;code&gt;app/&lt;/code&gt; — 98.8%&lt;/li&gt;
&lt;li&gt;Type coverage — 98.2%&lt;/li&gt;
&lt;li&gt;Mutation score — 98.9%&lt;/li&gt;
&lt;li&gt;Suite — 313 passed, 18 skipped, 933 assertions, about 16 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local Test Impact Analysis records which tests a change actually needs. CI still runs the full suite, including Chromium for the browser test. Coverage, evals, and mutate stay local.&lt;/p&gt;

&lt;p&gt;Coverage that never kills a mutant is decoration. Mutation score is the number I care about.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwo8ympeh7kwurtp3d84z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwo8ympeh7kwurtp3d84z.png" alt="Test run / coverage snapshot from the agents-testing post." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance is generated from the house
&lt;/h2&gt;

&lt;p&gt;Maintenance is not cleaning. Cleaning is mopping. Maintenance is the dryer vent, the gutters, the things that break if you forget them for a year.&lt;/p&gt;

&lt;p&gt;A House screen records the property: floors and rooms, outbuildings (storage, BBQ cabin), inside facilities (pool, SPA, jacuzzi), outside (garden, trees, flowers), and equipment (fridge, oven, laundry, dryer, dishwasher). Save the inventory and a schedule appears. You can override cadence, season, start date, skip, or notes.&lt;/p&gt;

&lt;p&gt;The Maintenance calendar is a month grid. Click a day for that day’s tasks. The same every-minute scheduler runs. At 08:00 in the user’s timezone, Laravel Notifications emails the day’s work. No tasks, no email.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdotmsjf20yl5e6315cih.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdotmsjf20yl5e6315cih.png" alt="House inventory: two floors, master bedroom, kitchen, BBQ cabin, indoor jacuzzi." width="800" height="611"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farb7p47ghushd8hl3dip.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farb7p47ghushd8hl3dip.png" alt="Generated maintenance schedule — dryer vent yearly, gutters every six months." width="800" height="611"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo1ugtwbw51mrpz1bs8k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo1ugtwbw51mrpz1bs8k.png" alt="September calendar with the 7th selected and that day’s tasks listed." width="800" height="778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A missed minute is late, not skipped
&lt;/h2&gt;

&lt;p&gt;Cooking, shopping, and maintenance all sat on that every-minute scheduler. A deploy or a crash in the due minute used to drop the task: serve the meal, mail “start cooking,” plan next week’s list, send the maintenance digest. The run was gone. Nothing tried again.&lt;/p&gt;

&lt;p&gt;The same jobs still tick every minute. They no longer need the clock to match the due minute. If the time has passed and the work is not marked done, they run it late. A delay is fine. Skipping is not.&lt;/p&gt;

&lt;p&gt;Same-day catch-up is bounded. Breakfast due at 08:00, scheduler back at 08:05: the meal is served. A prep mail due at 07:35 still goes. Last night’s dinner can still be served shortly after midnight. Yesterday’s breakfast is left alone.&lt;/p&gt;

&lt;p&gt;Shopping was the extra hole. The target week used to be computed from now plus lead time, so a miss that lasted into Monday slid to the following week. Catch-up now keeps the week that was due, as long as that Monday is not more than a day in the past, and passes those dates into the job so it does not plan the wrong week.&lt;/p&gt;

&lt;p&gt;A killed run used to hold a 24-hour overlap lock. That lock now expires in ten minutes. The queued jobs that plan meals and the shopping list retry after a worker crash, instead of failing on the first attempt.&lt;/p&gt;

&lt;p&gt;There is no extra “did it run?” table. The meal’s serve stamp, the prep-mail stamp, the shopping-list row, and the maintenance log already record whether the work ran.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz4rrhvxaaerxe3g9y0b.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz4rrhvxaaerxe3g9y0b.png" alt="Catch-up: the jobs run late instead of disappearing." width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Treat a missed cron as “run it when you wake up,” not “skip this one.”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning, from the same house
&lt;/h2&gt;

&lt;p&gt;Cleaning is the fourth original domain. It is generated from the same house inventory. It is kept off the maintenance list.&lt;/p&gt;

&lt;p&gt;Save the House screen and a cleaning schedule appears under the maintenance one. Kitchen counters daily. Vacuum and dust weekly. Bathroom weekly. Bedding weekly. Windows every three months. BBQ cabin weekly in summer. Pool skim weekly in summer. Dryer lint trap weekly — the yearly vent stays on maintenance.&lt;/p&gt;

&lt;p&gt;You can override cadence, season, start date, skip, or notes. Skip a task and that day stays empty.&lt;/p&gt;

&lt;p&gt;The Cleaning calendar is a month grid. Click a day for that day’s chores. At 08:00 local time, Laravel Notifications emails the work. No tasks, no email.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx65n4sg91m1avpgf7ic9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx65n4sg91m1avpgf7ic9.png" alt="Cleaning schedule generated from the house: change bedding weekly, clean kitchen sink weekly." width="800" height="330"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut8ylp2bq7ln51yed0ei.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut8ylp2bq7ln51yed0ei.png" alt="House cleaning schedule with counters daily, mop weekly, vacuum weekly." width="800" height="330"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7a3fumiji43szq8knx1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7a3fumiji43szq8knx1.png" alt="September cleaning calendar with a day selected and that day’s chores listed." width="800" height="367"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zorvf8u6apphbvxoiq3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zorvf8u6apphbvxoiq3.png" alt="The 08:00 cleaning mail." width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cook, shop, maintain, clean. That is the original four.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pieces lock
&lt;/h2&gt;

&lt;p&gt;The interesting part is not any one screen. It is the shared facts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cooking settings own meal times, timezone, units, and diet.&lt;/li&gt;
&lt;li&gt;Serving a meal decrements stock.&lt;/li&gt;
&lt;li&gt;Shopping subtracts stock from the week’s ingredients and writes a list with remainders.&lt;/li&gt;
&lt;li&gt;Lead time counts back from Monday breakfast, not from “Sunday night.”&lt;/li&gt;
&lt;li&gt;The house inventory generates two schedules: maintenance and cleaning. Dryer vent is maintenance. Dryer lint trap is cleaning.&lt;/li&gt;
&lt;li&gt;One scheduler, every minute, local time. Mail at 08:00 if there is work. Catch-up if the process was down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents produce structured output. The application records whether the work ran. I did not add a separate ledger for catch-up. The existing stamps were enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am testing in the Laravel AI SDK
&lt;/h2&gt;

&lt;p&gt;The public series started as a clean Laravel skeleton with the SDK already wired. The goal was to exercise the SDK in a real app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents and tools&lt;/li&gt;
&lt;li&gt;Structured outputs&lt;/li&gt;
&lt;li&gt;Conversation memory&lt;/li&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;li&gt;Testing, including evals and mutation testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cursor is the development environment. The application stays in Laravel and PHP. No separate agent runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;What I care about next is the same thing I cared about at the start. Does the agent still do the work when nobody is looking at the screen? If the answer is yes after a deploy at 07:59, the design is holding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built in public on Laravel, the Laravel AI SDK, Pest, and Cursor. Short updates go to X and LinkedIn. This is the long form.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Phone Validation in Laravel Using Abstract</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Mon, 17 Jun 2024 17:18:03 +0000</pubDate>
      <link>https://dev.to/kornatzky/phone-validation-in-laravel-using-abstract-f6l</link>
      <guid>https://dev.to/kornatzky/phone-validation-in-laravel-using-abstract-f6l</guid>
      <description>&lt;p&gt;Our sales events platform &lt;a href="https://auctibles.com" rel="noopener noreferrer"&gt;Auctibles&lt;/a&gt;, collects phone numbers for several purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Contact phone of sellers to be displayed to buyers&lt;/li&gt;
&lt;li&gt;Landline phone of sellers&lt;/li&gt;
&lt;li&gt;Mobile phone of sellers for coordination of deliveries&lt;/li&gt;
&lt;li&gt;Mobile phone of buyers for coordination of deliveries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Phone validation in Laravel is a crucial step for our sales events platform. It plays a significant role in ensuring smooth delivery coordination and reducing the risk of fraud. &lt;/p&gt;

&lt;p&gt;We use validation rules, a practical feature of the PHP Laravel framework, to ensure reliable validation. This is where the Abstract comes in, enhancing the functionality of our platform. &lt;/p&gt;

&lt;h1&gt;
  
  
  The Validation Rule
&lt;/h1&gt;

&lt;p&gt;We define a validation rule, &lt;code&gt;AbstractPhoneValidation&lt;/code&gt; that receives two parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The type of phone number expected - mobile, landline, or an empty string when any type is expected, &lt;/li&gt;
&lt;li&gt;The country of the phone number - two letters ISO 3166-1 alpha-2 code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule uses our Abstract API key from the configuration file.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    namespace App\Rules;

    use Closure;
    use Illuminate\Contracts\Validation\ValidationRule;
    use Illuminate\Support\Str;

    class AbstractPhoneValidation implements ValidationRule
    {
        /**
         * Create a new rule instance.
         *
         * @return void
         */
        public function __construct(
            private string $phone_type,
            private string $country,
        ) {}    


        /**
         * Run the validation rule.
         *
         * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
         */
        public function validate(string $attribute, mixed $value, Closure $fail): void
        {
            // Initialize cURL.
            $ch = curl_init();

            // Set the URL that you want to GET by using the CURLOPT_URL option.
            curl_setopt(
              $ch,
              CURLOPT_URL,
              'https://phonevalidation.abstractapi.com/v1/?api_key=' . config('app.ABSTRACT_PHONE_VERIFICATION_KEY') . "&amp;amp;phone=$value"
            );

            // Set CURLOPT_RETURNTRANSFER so that the content is returned as a variable.
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

            // Set CURLOPT_FOLLOWLOCATION to true to follow redirects.
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

            // Execute the request.
            $data = curl_exec($ch);

            // Close the cURL handle.
            curl_close($ch);

                    //The response from Abstract is a JSON
            $data = json_decode($data);

                    // here, we do the validation
            if (!$data-&amp;gt;valid || 
                // Is the phone number valid?
                ($this-&amp;gt;phone_type &amp;amp;&amp;amp; Str::lower($data-&amp;gt;type) != $this-&amp;gt;phone_type) || 
                // If the phone type was given, what the phone number of the required type?
                ($data-&amp;gt;country-&amp;gt;code != $this-&amp;gt;country)
                // Does the country of the phone correspond to the expected country?
            ) 
                    {
                $fail('validation.' . $attribute)-&amp;gt;translate();
            }

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

&lt;/div&gt;

&lt;p&gt;As Abstract returns the phone type capitalized, we use &lt;code&gt;Str::lower&lt;/code&gt; to convert it to lowercase to correspond to our labeling of phone types.&lt;/p&gt;

&lt;h1&gt;
  
  
  Using the Validation Rule
&lt;/h1&gt;

&lt;p&gt;We use the rule in validation, where the user's country two letters ISO 3166-1 alpha-2 code is stored in &lt;code&gt;$country&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$rules_array = [
    'mobile_phone' =&amp;gt; [new AbstractPhoneValidation('mobile', $country)],
    'landline_phone' =&amp;gt; [new AbstractPhoneValidation('landline', $country)],
    'contact_phone' =&amp;gt; [new AbstractPhoneValidation('', $this-&amp;gt;country)],
]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For a &lt;code&gt;contact_phone&lt;/code&gt;, any type of phone number is acceptable.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>abstract</category>
      <category>phone</category>
    </item>
    <item>
      <title>Emulate User Activity with Bots</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Sun, 09 Jun 2024 17:26:03 +0000</pubDate>
      <link>https://dev.to/kornatzky/emulate-user-activity-with-bots-5hdd</link>
      <guid>https://dev.to/kornatzky/emulate-user-activity-with-bots-5hdd</guid>
      <description>&lt;p&gt;In our sales events platform, &lt;a href="https://auctibles.com" rel="noopener noreferrer"&gt;Auctibles&lt;/a&gt;, sellers have the exciting opportunity to create various events. These range from a sale event at a fixed price to live auctions, where buyers bid the price up, providing a dynamic and engaging experience. &lt;/p&gt;

&lt;h1&gt;
  
  
  The Technology
&lt;/h1&gt;

&lt;p&gt;We use the Laravel framework in the context of the TALL stack: Tailwind, Alpine.js, Laravel, and Livewire. &lt;/p&gt;

&lt;h1&gt;
  
  
  Why Emulation?
&lt;/h1&gt;

&lt;p&gt;For a demo environment, it's crucial that we accurately emulate buyers'  activity for a seller. This includes sending buy orders and chat messages. The purpose of the emulation is to allow the seller to get a feeling of how the event with multiple buyers will be observed from the event control screens. This includes changing the quantities of items still available, dynamic price changes, and chat messages from buyers.&lt;/p&gt;

&lt;h1&gt;
  
  
  Ordinary Buyers
&lt;/h1&gt;

&lt;p&gt;The buy screen is a Livewire component. Clicking the BUY button sends a broadcast event: &lt;code&gt;LiveBuy,&lt;/code&gt; &lt;code&gt;LiveBid&lt;/code&gt;,... &lt;/p&gt;

&lt;p&gt;Similarly, buyers send chat messages via a broadcast event ChatMessage. &lt;/p&gt;

&lt;h1&gt;
  
  
  Bots for Emulation
&lt;/h1&gt;

&lt;p&gt;We emulate buyers' activity via a bot. The bot is a ReactPHP server that periodically sends the same broadcast events that ordinary buyers send. &lt;/p&gt;

&lt;p&gt;The server receives a &lt;code&gt;set_timer&lt;/code&gt; GET call at the event's start and a &lt;code&gt;stop_timer&lt;/code&gt; GET call at its end. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$loop = Factory::create();
$server = new Server(function (ServerRequestInterface $request) use($loop) {

        $path = $request-&amp;gt;getUri()-&amp;gt;getPath();
        $method = $request-&amp;gt;getMethod();

        if ($path == '/set_timer' &amp;amp;&amp;amp; $method === 'GET') {

          $params = $request-&amp;gt;getQueryParams();
                $event_id = $params['event_id'] ?? null;

                // start emulation

                return Response::plaintext('timer set')-&amp;gt;withStatus(Response::STATUS_OK);   

            } else if ($path == '/stop_timer' &amp;amp;&amp;amp; $method === 'GET') {

            $params = $request-&amp;gt;getQueryParams();
                $event_id = $params['event_id'] ?? null;   

                // stop emulation

                return Response::plaintext('timer stopped')-&amp;gt;withStatus(Response::STATUS_OK);               
                    }

});

$socket = new SocketServer('0.0.0.0:' . config('app.BOT_SERVER_PORT'));
$server-&amp;gt;listen($socket);

$loop-&amp;gt;run();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Laravel to ReactPHP
&lt;/h1&gt;

&lt;p&gt;These calls are received from the component where the seller controls the event, using  &lt;code&gt;Http&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $url = config('app.BOT_SERVER_URL') . ':' . config('app.BOT_SERVER_PORT') . '/set_timer';
  $data = [
  'event_id' =&amp;gt; $this-&amp;gt;event-&amp;gt;id,
  ];

$response = Http::withQueryParameters($data)-&amp;gt;get($url);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Starting the Bot
&lt;/h1&gt;

&lt;p&gt;To start the emulation, we set a periodic timer, &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$timer = $loop-&amp;gt;addPeriodicTimer($this-&amp;gt;period_in_seconds, function () use ($event_id) {

   $this-&amp;gt;send_bid($event_id);
   $this-&amp;gt;send_message($event_id);

});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We store the timer in an array:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$this-&amp;gt;timers[$event_id] = $timer;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Stopping the Bot
&lt;/h1&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$loop-&amp;gt;cancelTimer($this-&amp;gt;timers[$event_id]);
unset($this-&amp;gt;timers[$event_id]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>laravel</category>
      <category>ecommerce</category>
      <category>bots</category>
      <category>php</category>
    </item>
    <item>
      <title>Using Disposable Emails for a Demo</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Tue, 04 Jun 2024 12:34:40 +0000</pubDate>
      <link>https://dev.to/kornatzky/using-disposable-emails-for-a-demo-130m</link>
      <guid>https://dev.to/kornatzky/using-disposable-emails-for-a-demo-130m</guid>
      <description>&lt;p&gt;We have a demo environment in &lt;a href="https://auctibles.com" rel="noopener noreferrer"&gt;Auctibles&lt;/a&gt;, the dynamic prices and sales events platform. &lt;/p&gt;

&lt;h1&gt;
  
  
  Demo Compared to Production
&lt;/h1&gt;

&lt;p&gt;The demo environment has: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Emulated shipping that takes 10 minutes in each direction.&lt;/li&gt;
&lt;li&gt;Emulated payment - you click the &lt;code&gt;Pay Now&lt;/code&gt; button to pay, and the payment is immediately executed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Remove Friction in the Demo
&lt;/h1&gt;

&lt;p&gt;Asking people to enter their email for signup will be a central friction point in the demo. To test the demo's functionality, our customer must create several users in our demo environment. &lt;/p&gt;

&lt;p&gt;Users for testing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One seller user&lt;/li&gt;
&lt;li&gt;Multiple buyer users &lt;/li&gt;
&lt;li&gt;Optionally team member users - as a seller can operate a team&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The effort to create multiple emails might cause our customers to drop.&lt;/p&gt;

&lt;h1&gt;
  
  
  Disposable Emails
&lt;/h1&gt;

&lt;p&gt;Several providers supply temporary disposable emails that you can use for a few days. So, we decided to allow our customers to use disposable emails in the demo environment. &lt;/p&gt;

&lt;p&gt;So now our signup page has a button to show the customer how to get a disposable email: &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%2Fn9psow5ygx6kucnfjkcu.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%2Fn9psow5ygx6kucnfjkcu.png" alt=" " width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This opens a modal with the list of disposable email providers:&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%2Fkutsa9qz8vvbgbfekhqx.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%2Fkutsa9qz8vvbgbfekhqx.png" alt=" " width="799" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Not Allow Disposable Emails in the Production
&lt;/h1&gt;

&lt;p&gt;At the risk of explaining the obvious, a seller or buyer must use the platform for several days to complete the purchase. The seller has to create a sales event, which will be held days before the event. &lt;/p&gt;

&lt;p&gt;After the event, the buyer has to pay and receive the item via delivery. Subsequently, according to consumer protection laws, the buyer is allowed to return the item within 14 days.  &lt;/p&gt;

&lt;p&gt;These disposable emails are usable only for a few days, so they cannot be practically used in production. &lt;/p&gt;

&lt;h1&gt;
  
  
  Cyber Risks
&lt;/h1&gt;

&lt;p&gt;Disposable emails create a cyber risk. It is easy for malicious actors to create disposable emails. &lt;/p&gt;

&lt;p&gt;This is why E-Commerce and SAAS websites often prevent users from using disposable emails. &lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>demo</category>
      <category>email</category>
      <category>disposable</category>
    </item>
    <item>
      <title>Using ApyHub for Image Moderation</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Fri, 31 May 2024 09:05:25 +0000</pubDate>
      <link>https://dev.to/kornatzky/using-apyhub-for-image-moderation-4gg1</link>
      <guid>https://dev.to/kornatzky/using-apyhub-for-image-moderation-4gg1</guid>
      <description>&lt;p&gt;In &lt;a href="https://auctibles.com" rel="noopener noreferrer"&gt;Auctibles&lt;/a&gt;, we use &lt;a href="https://apyhub.com" rel="noopener noreferrer"&gt;ApyHub&lt;/a&gt; to moderate images of items for auction. Auctibles is built using the Laravel PHP framework. &lt;/p&gt;

&lt;h2&gt;
  
  
  Uploading Image Files
&lt;/h2&gt;

&lt;p&gt;Videos are effortlessly uploaded using a straightforward Livewire component and an HTML &lt;code&gt;input&lt;/code&gt; element with &lt;code&gt;type=file&lt;/code&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input id="photos" name="photos" type="file" class="sr-only" wire:model.live="photos" accept="image/png,image/jpg,image/gif"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The component has a public property:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public $photos = [];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Upon clicking a &lt;code&gt;submit&lt;/code&gt; button, we apply validation to the field, &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'photos' =&amp;gt; 'nullable|array|max:3', // array
'photos.*' =&amp;gt; [
    'required',
    'image',
    'max:10240', // 10MB
    new ExplicitImage(),
],  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ExplicitImage&lt;/code&gt; is a validation rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporary Files
&lt;/h2&gt;

&lt;p&gt;We upload temporary files to a Minio bucket which resides on the server. The bucket is defined as an &lt;code&gt;uploads&lt;/code&gt; bucket for Laravel. This is done in &lt;code&gt;config/filesystems.php&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ApyHub Results for Image Content
&lt;/h2&gt;

&lt;p&gt;We use &lt;code&gt;curl&lt;/code&gt; to call ApyHub. The response looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "data": {
    "apyhub": {
      "adult": {
        "adultScore": 0.0025164163671433926,
        "goreScore": 0.0014069777680560946,
        "isAdultContent": false,
        "isGoryContent": false,
        "isRacyContent": false,
        "racyScore": 0.0032450903672724962
      },
      "metadata": {
        "format": "Png",
        "height": 1024,
        "width": 1024
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  The Validation Rule
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use CURLFile;
use CURLStringFile;
use Illuminate\Support\Facades\Storage;

class ExplicitImage implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
            // path to temporary uploaded file in uploads disk
        $path = config('livewire')['temporary_file_upload']['directory'] . DIRECTORY_SEPARATOR . $value-&amp;gt;getFilename();

        $ch = curl_init();

        curl_setopt(
          $ch,
          CURLOPT_URL,
          "https://api.apyhub.com/ai/image/detect/explicit-content/file"
        );
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
          'apy-token: ' . config('app.APYHUB_TOKEN'),
          'content-type: multipart/form-data'
        ]);
        curl_setopt($ch, CURLOPT_POSTFIELDS, [
            'file' =&amp;gt; new CURLStringFile(Storage::disk('uploads')-&amp;gt;get($path), $value-&amp;gt;getFilename(), $value-&amp;gt;getMimeType()),
            'requested_service' =&amp;gt; 'apyhub',
        ]);

        $response = curl_exec($ch);

        curl_close($ch);

        if ($response === false) {
          $error = curl_error($ch);
          logger()-&amp;gt;warning("ApyHub image moderation CURL Error: $error");
          return;
        } else {
          // Process the successful CURL call here.
          $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

          if ($statusCode == 200) {
            // The request was successful
            $data = json_decode($response, true); //

            $response = $data['data']['apyhub']['adult'];

            if ($response['isAdultContent'] || $response['isGoryContent'] || $response['isRacyContent']) {
                $fail('validation.' . 'explicit_image')-&amp;gt;translate(); 
            }

          } else {
            // The server responded with an error
            logger()-&amp;gt;warning("ApyHub image moderation HTTP Error: $statusCode");
            return;
          }
        }

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

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>laravel</category>
      <category>php</category>
      <category>apyhub</category>
    </item>
    <item>
      <title>Use SharpAPI for Translating E-Commerce Product Info</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Tue, 28 May 2024 06:38:30 +0000</pubDate>
      <link>https://dev.to/kornatzky/use-sharpapi-for-translating-e-commerce-product-info-40e8</link>
      <guid>https://dev.to/kornatzky/use-sharpapi-for-translating-e-commerce-product-info-40e8</guid>
      <description>&lt;p&gt;In &lt;a href="https://auctibles.com" rel="noopener noreferrer"&gt;Auctibles&lt;/a&gt;, we use &lt;a href="https://sharpapi.com" rel="noopener noreferrer"&gt;SharpAPI&lt;/a&gt; to translate product info. &lt;/p&gt;

&lt;p&gt;Auctibles is built with the Laravel PHP framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Service
&lt;/h2&gt;

&lt;p&gt;We defined a service that will translate a text into a given language.&lt;/p&gt;

&lt;p&gt;The service provides a &lt;code&gt;translate&lt;/code&gt; function that translates the &lt;code&gt;text&lt;/code&gt; parameter into the required language:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
namespace App\Services;

use GuzzleHttp\Exception\ClientException;

class SharpAPI
{
    private $service;

    public function __construct()
    {
    }

    public function translate(string $text, string $to_language)
    {       

        try {

            $statusUrl = \SharpApiService::translate($text, $to_language);

        } catch(ClientException $e) {

            $rsp = $e-&amp;gt;getResponse();
            return ['flag' =&amp;gt; null];

        }

        $result = \SharpApiService::fetchResults($statusUrl);
        $res = json_decode($result-&amp;gt;getResultJson());

        return ['flag' =&amp;gt; true, 'content' =&amp;gt; $res-&amp;gt;content];
    }

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

&lt;/div&gt;

&lt;p&gt;The function returns an associative array, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bool flag&lt;/code&gt; - signifying success or failure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content&lt;/code&gt; - translated content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Job
&lt;/h2&gt;

&lt;p&gt;Because calling SharpAPI takes time, we do this asynchronously in a job, once the user saves the product information. &lt;/p&gt;

&lt;p&gt;The job takes an &lt;code&gt;$obj&lt;/code&gt; parameter, which is the product, and an array of &lt;code&gt;$fields&lt;/code&gt; to be translated. The job iterates over the fields and sends each one to the service for translation.&lt;/p&gt;

&lt;p&gt;The object comes from an Eloquent model using &lt;a href="https://spatie.be/docs/laravel-translatable/v6/introduction" rel="noopener noreferrer"&gt;Laravel-Translatable&lt;/a&gt;. So each field is a JSON array, mapping languages to the value for that language.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Jobs\AI;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

use SharpAPI\SharpApiService\Enums\SharpApiLanguages;

use App\Services\SharpAPI;

class SharpAPITranslator implements ShouldQueue, ShouldBeEncrypted
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     */
    public function __construct(private string $from_language, private $obj, private $fields)
    {

    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
            // instantiate the service
        $sharp_api = new SharpAPI();

        foreach($this-&amp;gt;fields as $field) 
        {
                        foreach(array_keys(config('app.content_languages')) as $to_language)
            {
                if ($to_language != $this-&amp;gt;from_language) {

                                        // get the content for the from_language
                    $text = $obj-&amp;gt;getTranslation($field, $fromlang, true)


                    $result = $sharp_api-&amp;gt;translate(
                        $text, 
                        $this-&amp;gt;sharp_api_language($to_language)
                    );


                    if ($result['flag']) {
                            // store the content for to_language
                        $this-&amp;gt;obj-&amp;gt;setTranslation(
                            $field, 
                            $to_language,
                            $result['content']
                        );
                    }


                }
            }

            $this-&amp;gt;obj-&amp;gt;save();

        }

    }

    private function sharp_api_language(string $locale): string
    {
        switch($locale)
        {
            case 'en':
                return SharpApiLanguages::ENGLISH;

            case 'pl':
                return SharpApiLanguages::POLISH;

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

&lt;/div&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>php</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Integrating a ReactPHP Server in Laravel</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Sat, 25 May 2024 10:42:33 +0000</pubDate>
      <link>https://dev.to/kornatzky/integrating-a-reactphp-server-in-laravel-lo0</link>
      <guid>https://dev.to/kornatzky/integrating-a-reactphp-server-in-laravel-lo0</guid>
      <description>&lt;h2&gt;
  
  
  Create a Command
&lt;/h2&gt;

&lt;p&gt;Create a Laravel command, &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php artisan make:command SaleServer --command=bidserver:sale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This command will be a daemon that runs a ReactPHP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling the Server
&lt;/h2&gt;

&lt;p&gt;The command is called with a HTTP &lt;code&gt;post&lt;/code&gt; from a Livewire component, &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Http::asForm()-&amp;gt;post(config('auctions.SALE_SERVER_URL') . ':' .  config('auctions.SALE_SERVER_PORT') . '/buy', [
    'auction_id' =&amp;gt; $this-&amp;gt;auction-&amp;gt;id,
    'item_id' =&amp;gt; $this-&amp;gt;item-&amp;gt;id,
    'user' =&amp;gt; $this-&amp;gt;user-&amp;gt;id,
    'price' =&amp;gt; $bid_received['price'],
]);   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  The Server
&lt;/h2&gt;

&lt;p&gt;The command creates a ReactPHP server, that receives these calls.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Console\Commands;


use Illuminate\Console\Command;
use React\Http\Server;
use React\Http\Message\Response;
use React\EventLoop\Factory;
use Psr\Http\Message\ServerRequestInterface;
use React\Socket\SocketServer;

class SaleServer extends Command
{

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'bidserver:sale';

/**
 * The console command description.
 *
 * @var string
 */
protected $description = 'Sale bid server';

/**
 * Execute the console command.
 */
public function handle()
{
    $loop = Factory::create();
    $server = new Server(function (ServerRequestInterface $request) {

        $path = $request-&amp;gt;getUri()-&amp;gt;getPath();
        $method = $request-&amp;gt;getMethod();

                //Check if the path and method of the call are correct
        if ($path == '/buy'  &amp;amp;&amp;amp; $method === 'POST') {

                    //Extract the call parameters
        $auction_id = $request-&amp;gt;getParsedBody()['auction_id'] ?? null;
        $item_id = $request-&amp;gt;getParsedBody()['item_id'] ?? null;
        $user = $request-&amp;gt;getParsedBody()['user'] ?? null; 
        $bid_price = $request-&amp;gt;getParsedBody()['price'] ?? null; 

          //Broadcast a response
          broadcast(new BuyAccepted($auction_id, $item_id, $user))-&amp;gt;toOthers();

                    return Response::plaintext('bid processed')-&amp;gt;withStatus(Response::STATUS_OK);   

    }             

    return Response::plaintext('Not found')-&amp;gt;withStatus(Response::STATUS_NOT_FOUND);

        });

    $socket = new SocketServer('0.0.0.0:' . config('auctions.SALE_SERVER_PORT'));
    $server-&amp;gt;listen($socket);

  $loop-&amp;gt;run();

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

&lt;/div&gt;
&lt;h2&gt;
  
  
  Daemon in Forge
&lt;/h2&gt;

&lt;p&gt;Add a daemon running the command:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php artisan bidserver:sale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>reactphp</category>
    </item>
    <item>
      <title>What to Monitor in a Website</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Thu, 15 Dec 2022 11:30:42 +0000</pubDate>
      <link>https://dev.to/kornatzky/what-to-monitor-in-a-website-28c6</link>
      <guid>https://dev.to/kornatzky/what-to-monitor-in-a-website-28c6</guid>
      <description>&lt;h2&gt;
  
  
  The Conundrum of Monitoring
&lt;/h2&gt;

&lt;p&gt;So you put up your website in the cloud, and your hosting company, cloud providers, and all sorts of people offer you to monitor your website. &lt;/p&gt;

&lt;h2&gt;
  
  
  Launching A Platform
&lt;/h2&gt;

&lt;p&gt;Now that I am approaching the launch of my platform for auction events for creators, I am putting it up in the cloud. So I need to create a server where the platform will run. Having done that, I encountered so many options to monitor it.&lt;/p&gt;

&lt;p&gt;Despite having twenty-five years of industrial experience, I still found my head swirling from all these offers. In all the IT companies I worked for, we had a DevOps or infrastructure team to monitor whatever we needed.&lt;/p&gt;

&lt;p&gt;So I had to make some order in my thoughts. For this purpose, I created the drawing you see at the top of this article. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Web Application and the Scheduler
&lt;/h2&gt;

&lt;p&gt;We have a web application, a website with screens, and a database. For any platform, there are some jobs that we need to do every day, every week, or every month. Such jobs include sending monthly invoices. Thus, we need a scheduler to run these jobs at the required frequency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should We Monitor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Uptime
&lt;/h3&gt;

&lt;p&gt;We want to know that the website is up and running. This is called &lt;strong&gt;uptime&lt;/strong&gt;. The uptime monitor has to run outside the server and the platform. Because otherwise, you would not know that your server is down. &lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;Websites and servers can get overloaded, either temporarily or permanently. Temporary spikes can be due to particular days of the year, like the holiday shopping season. Permanent overload points to the need to increase the resources available. &lt;/p&gt;

&lt;p&gt;Resource monitors require some agent to be attached to your website to report the CPU, memory, or disk load. The resource monitor can reside inside or outside the server. In many cases, you will use both. An internal monitor is cheaper but can be clogged down in overload. &lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic Jobs
&lt;/h3&gt;

&lt;p&gt;Did the jobs run on time? Were they successfully executed?&lt;/p&gt;

&lt;p&gt;You can record somewhere on the server whether the job is finished. But you must monitor outside the server to see if a job was successfully executed. The reason is that if the job did not run, no one inside could tell you that. Maybe the scheduler is dead.&lt;/p&gt;

&lt;p&gt;So each regular job has to report to an outside monitor on its execution. And the monitor that has a list of jobs and frequencies can compare these reports to what happened and alert you for any malfunction.&lt;/p&gt;

&lt;h2&gt;
  
  
  An External Scheduler
&lt;/h2&gt;

&lt;p&gt;Due to all the complications of scheduling and monitoring, numerous companies offer to schedule jobs for you by calling some endpoint (function) in your platform. Such offers are often named &lt;strong&gt;cron&lt;/strong&gt; something - reminiscent of the Linux &lt;em&gt;cron&lt;/em&gt; scheduler. &lt;/p&gt;

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

&lt;p&gt;I hope that now you can better sort all the different options for monitoring and make a cost-effective choice.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>monitoring</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>The Timeline Method for Determined Construction of Web Applications</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Thu, 24 Nov 2022 13:28:47 +0000</pubDate>
      <link>https://dev.to/kornatzky/the-timeline-method-for-determined-construction-of-web-applications-cdg</link>
      <guid>https://dev.to/kornatzky/the-timeline-method-for-determined-construction-of-web-applications-cdg</guid>
      <description>&lt;p&gt;How do you approach the development of a new web application? If you are a solopreneur or SMB, you are not aiming for a big scale. You cannot afford well-known but costly development environments.&lt;/p&gt;

&lt;p&gt;It is a challenge that I encountered repeatedly as a freelance consultant. Build it and ship it without overbuilding.&lt;/p&gt;

&lt;p&gt;Here is the method that I usually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Matrix
&lt;/h2&gt;

&lt;p&gt;Think of the complete development process as a two-dimensional plane.&lt;/p&gt;

&lt;p&gt;On the horizontal axis, we have the different phases of the evolution of the web application. This is a timeline of phases that it goes through. &lt;/p&gt;

&lt;p&gt;The vertical time axis is the computing environment you will use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Time Axis - Phases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Collection of emails&lt;/li&gt;
&lt;li&gt;Forthcoming - add information pages&lt;/li&gt;
&lt;li&gt;Production - add sign up/sign in and rea; functionality&lt;/li&gt;
&lt;li&gt;Add testimonials and other forms of social proof&lt;/li&gt;
&lt;li&gt;Scale up - multiple servers, load balancing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of course, the production phase of development will be broken into multiple sub-phases. &lt;/p&gt;

&lt;p&gt;In the action events platform I am building, we start with notifications via email. Later we will add notifications through SMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Environments
&lt;/h2&gt;

&lt;p&gt;Each phase has to go through these environments,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local development&lt;/li&gt;
&lt;li&gt;Staging - cloud environment&lt;/li&gt;
&lt;li&gt;Production - production environment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason for going through this sequence is that we need to test things before we release them.&lt;/p&gt;

&lt;p&gt;We start with local development but must move to the cloud to demo anything. &lt;/p&gt;

&lt;h2&gt;
  
  
  Design Backward from Production
&lt;/h2&gt;

&lt;p&gt;We design the production web application in a most general form. Then we define a subset within the general framework for the collection and the forthcoming phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Structure
&lt;/h2&gt;

&lt;p&gt;As we would like the collection and forthcoming phases to have the production look and feel, we work by working backward by focusing on the navigation bar and the home page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Bar
&lt;/h3&gt;

&lt;p&gt;A navigation bar with all pages that will be in production. We expose more and more pages as we proceed through the phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Home Page
&lt;/h3&gt;

&lt;p&gt;The collection and forthcoming pages have the general look and feel but with an email collection form only. To build a mailing list. &lt;/p&gt;

&lt;p&gt;In the production phase, we move to a real home page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Pages
&lt;/h3&gt;

&lt;p&gt;If any other page appears in the collection or forthcoming phase, structure it to represent the corresponding content for that phase if different from the production phase.&lt;/p&gt;

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

&lt;p&gt;With this approach, you construct one web application for all the phases you encounter. &lt;/p&gt;

&lt;p&gt;Thereby you get a uniform look and feel, with a single simple timeline for development, for all phases. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>website</category>
      <category>development</category>
    </item>
    <item>
      <title>Experience Continuity in Hybrid Events Platforms</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Fri, 04 Nov 2022 11:39:46 +0000</pubDate>
      <link>https://dev.to/kornatzky/experience-continuity-in-hybrid-events-platforms-48d3</link>
      <guid>https://dev.to/kornatzky/experience-continuity-in-hybrid-events-platforms-48d3</guid>
      <description>&lt;p&gt;You are building a hybrid events platform, such as the Auction Events Platform for Creators I am making. Users will be on and off the website during the event. How do you achieve continuity of the experience for your users?&lt;/p&gt;

&lt;h1&gt;
  
  
  Define Meaningful Moments
&lt;/h1&gt;

&lt;p&gt;Choose moments when there is some phase change, such as the start of a session or lecture. In my auction platform, this is when an item (lot) comes up for auction during a live auction. &lt;/p&gt;

&lt;p&gt;Broadcast these meaningful events to users via email, SMS, etc., so that users that are off the website know about it. &lt;/p&gt;

&lt;h1&gt;
  
  
  Define Sub Moments
&lt;/h1&gt;

&lt;p&gt;In many event platforms, such moments are spread in time for an hour or more, during which time meaningful things happen. &lt;/p&gt;

&lt;p&gt;In my auction events platform, we have &lt;strong&gt;timed auctions&lt;/strong&gt; that run automatically from start time to end time. In a timed auction, all items come up for auction at the start time. An auction can last for days. So a user might bid and then go off to his/her life. You need to tell the user when a competing bid comes up and is on top. So the user can come back and bid again at a higher price. &lt;/p&gt;

&lt;p&gt;In a conference session, such sub-moments could be the switch to Q&amp;amp;A at the end of a presentation. In a concert platform, a sub-moment is when the band starts to play a new song. The way Apple Music on a Mac shows a notification when a new song start. &lt;/p&gt;

&lt;h1&gt;
  
  
  Detect In-Person Presence
&lt;/h1&gt;

&lt;p&gt;In a hybrid event, you want to turn off notifications for users that present IRL (in real life), as these notifications will distract them from what is happening IRL.&lt;/p&gt;

&lt;p&gt;In the auction events platform, users present in person will be given a sign with their user number with which to bid. We will use the opportunity to record their presence on the platform and avoid sending notifications to them.&lt;/p&gt;

&lt;h1&gt;
  
  
  Distinguish Pre-Event and Post-Event Phases
&lt;/h1&gt;

&lt;p&gt;In many event platforms and webinars, there will be interaction before and after the event. Such as chatting with organizers or speakers. In our auction events platform, users can chat with creators before, during, and after the live auction. &lt;/p&gt;

&lt;p&gt;In the pre-event and post-event phases, you can expect users to be off the website for extended periods. And messages may come at any time. So we need to forward any message to users with a high probability of not being online in the platform via email or SMS. &lt;/p&gt;

&lt;p&gt;During a live auction, such forwarding is not needed and is distracting from the attention on the bidding. &lt;/p&gt;

&lt;h1&gt;
  
  
  Conculsion
&lt;/h1&gt;

&lt;p&gt;By now, most of us have participated in some virtual or hybrid conference and received a barrage of email messages around it. The snowball effect is jarring and counter-productive.&lt;/p&gt;

&lt;p&gt;Micro-designing notifications and messaging from the standpoint of experience continuity promises a much more pleasant user experience and will lead to higher engagement.  &lt;/p&gt;

</description>
      <category>auctions</category>
      <category>hybridevent</category>
      <category>realtimeweb</category>
      <category>userinterface</category>
    </item>
    <item>
      <title>Generic Design for a Creator Economy Platform</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Wed, 05 Oct 2022 10:24:09 +0000</pubDate>
      <link>https://dev.to/kornatzky/generic-design-for-a-creator-economy-platform-jej</link>
      <guid>https://dev.to/kornatzky/generic-design-for-a-creator-economy-platform-jej</guid>
      <description>&lt;p&gt;You want to build a creator economy platform. It could be a newsletters platform, like Revue or Substack, a podcasting platform like Anchor or Transistor, or a crowd-funding platform, like Patreon or Buy Me Coffee.&lt;/p&gt;

&lt;p&gt;How do you design such a platform? There are many moving parts and infinite possibilities. Yet, after studying many creator economy platforms, we constructed a generic design you can use to build your platform.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Two Faces of Creator Economy Platform
&lt;/h1&gt;

&lt;p&gt;A creator economy platform wears two faces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A consumer-facing face - the link the creator gives to its fans to see the profile, newsletter, podcast, etc.&lt;/li&gt;
&lt;li&gt;A creator-facing face - the interface through which the creator creates content, such as uploading a new podcast, writing the profile, and tweaking the consumer-facing face&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  The Consumer-Facing Face
&lt;/h1&gt;

&lt;p&gt;The consumer-facing face is a website, possibly with a custom domain or custom subdomain.&lt;/p&gt;

&lt;p&gt;The website presents the creator and its activities. Its specific content will be defined by what the creator economy platform is doing. &lt;/p&gt;

&lt;p&gt;For a newsletters platform, the website will present the newsletter information, the creator information, and a listing of newsletter issues. The front page will include a call-to-action (CTA) for a subscription to the newsletter.&lt;/p&gt;

&lt;p&gt;Generally, this website is either a one-pager or has two levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creator information and list of creations (e.g., newsletter issues, podcast episodes)&lt;/li&gt;
&lt;li&gt;A page for each creation (e.g., podcast episode)&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  The Creator-Facing Face
&lt;/h1&gt;

&lt;p&gt;The creator-facing face is foremost a website builder - for generating and tweaking the consumer-facing face.&lt;/p&gt;

&lt;p&gt;This includes several generic components. &lt;/p&gt;

&lt;h2&gt;
  
  
  Profile Creator
&lt;/h2&gt;

&lt;p&gt;The profile editor sets creator details.&lt;/p&gt;

&lt;p&gt;The profile creator tweaks the website theme and style and provides a setup for the essential assets for the website, such as the profile photo and background. &lt;/p&gt;

&lt;h2&gt;
  
  
  Creation Board
&lt;/h2&gt;

&lt;p&gt;The board is where you create content. It includes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Details Form
&lt;/h3&gt;

&lt;p&gt;A form with inputs for creation name and additional details, and panels for uploading assets, such as cover images&lt;/p&gt;

&lt;h3&gt;
  
  
  Rich Text Editor
&lt;/h3&gt;

&lt;p&gt;Most creations involve some long-form text. For a newsletters platform, it is a newsletter editor. For a podcast platform, it is an episode editor. &lt;/p&gt;

&lt;h3&gt;
  
  
  Rich Media Interface
&lt;/h3&gt;

&lt;p&gt;Most creations are media, such as audio for podcasts or video for courses. The rich media interface is where you upload the media. &lt;/p&gt;

&lt;p&gt;Through the interface, you may further process media, such as changing formats. &lt;/p&gt;

&lt;h2&gt;
  
  
  Integrations Setup
&lt;/h2&gt;

&lt;p&gt;For most creator economy platforms, the website generated is just the presentation layer. The content has to be distributed from the platform to the consumer. If it is a newsletter, you need to mail it. If it is a podcast, you need to distribute it to podcast platforms such as Spotify or Apple Podcasts.&lt;/p&gt;

&lt;p&gt;Hence each creator economy platform needs a section of specific integration to enable this distribution for the creator. We cannot give a generic design here. &lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusions
&lt;/h1&gt;

&lt;p&gt;Hopefully, our generic design will kickstart the design of your platform. &lt;/p&gt;

</description>
      <category>creatoreconomy</category>
      <category>websitebuilder</category>
      <category>website</category>
      <category>websitedesign</category>
    </item>
    <item>
      <title>Custom Domain Support in Creator Economy Platforms</title>
      <dc:creator>Yoram Kornatzky</dc:creator>
      <pubDate>Fri, 23 Sep 2022 16:10:35 +0000</pubDate>
      <link>https://dev.to/kornatzky/custom-domain-support-in-creator-economy-platforms-2hl0</link>
      <guid>https://dev.to/kornatzky/custom-domain-support-in-creator-economy-platforms-2hl0</guid>
      <description>&lt;p&gt;Most creator platforms support two forms of custom domains: \&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subdomains - such as &lt;em&gt;johndoe.platformX.com&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Custom domain - such as &lt;em&gt;platformX.johndoe.com&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Describing the creator domain as part of the URL of the site, such as &lt;em&gt;&lt;a href="http://www.platformX.com/johndoe" rel="noopener noreferrer"&gt;www.platformX.com/johndoe&lt;/a&gt;&lt;/em&gt;, is much less common.&lt;/p&gt;

&lt;p&gt;If you are about to construct such a platform, you may wonder how complicated it is to build custom domain support. &lt;/p&gt;

&lt;p&gt;It turns out it is straightforward. &lt;/p&gt;

&lt;p&gt;It boils down to two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user doing setup in their domain registrar or website hosting company&lt;/li&gt;
&lt;li&gt;Routing in your platform&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Domain Setup by User
&lt;/h1&gt;

&lt;p&gt;You should instruct users to add appropriate DNS records in their domain registrar or hosting company. There are many examples of instructions in existing platforms that you can borrow. &lt;/p&gt;

&lt;p&gt;The &lt;a href="https://support.transistor.fm/en/article/using-a-custom-domain-for-your-podcast-website-ivsoyr/" rel="noopener noreferrer"&gt;Transistor podcasting platform&lt;/a&gt; instructions that I recently used are excellent.&lt;/p&gt;

&lt;h1&gt;
  
  
  Routing in Platform
&lt;/h1&gt;

&lt;p&gt;While I do not know which backend programming framework or language you use, the following in &lt;a href="https://laravel.com" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt; PHP is pretty generic and can be adapted to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any PHP framework&lt;/li&gt;
&lt;li&gt;Express.js or any Node.js backend framework&lt;/li&gt;
&lt;li&gt;Ruby on Rails&lt;/li&gt;
&lt;li&gt;Java Spring Boot&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Domains
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route::group(['domain' =&amp;gt; '{subdomain}.{domain}.{tld}'], function(){

    Route::any('/', function($sub, $domain, $tld){
        return 'custom domain: ' .  
            $sub . '. ' . 
            $domain .  '. ' . 
            $tld;
    });

});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Subdomain
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route::domain('{account}.example.com')-&amp;gt;group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>laravel</category>
      <category>domain</category>
      <category>php</category>
      <category>dns</category>
    </item>
  </channel>
</rss>
