<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>Motion, Stylus, Trackpads And Foldables In The Core</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:23:42 +0000</pubDate>
      <link>https://dev.to/codenameone/motion-stylus-trackpads-and-foldables-in-the-core-4m87</link>
      <guid>https://dev.to/codenameone/motion-stylus-trackpads-and-foldables-in-the-core-4m87</guid>
      <description>&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%2F8tjad9hxrpn01ja0dtz3.jpg" 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%2F8tjad9hxrpn01ja0dtz3.jpg" alt="Motion, Stylus, Trackpads And Foldables In The Core" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time, mobile meant a touch screen and a few platform-specific escape hatches. That is no longer enough. A Codename One app can run on phones, tablets, desktops, watches, TVs, browsers, foldables, touch laptops, external displays, cars, and devices with pens, mice, and trackpads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5310" rel="noopener noreferrer"&gt;PR #5310&lt;/a&gt; and &lt;a href="https://github.com/codenameone/CodenameOne/pull/5309" rel="noopener noreferrer"&gt;PR #5309&lt;/a&gt; make that hardware visible through core APIs instead of cn1libs and platform branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion Sensors And Gestures
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;com.codename1.sensors&lt;/code&gt; package exposes accelerometer, gyroscope and magnetometer readings. It also derives gravity, linear acceleration and orientation in the core when the platform does not provide them directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt; &lt;span class="n"&gt;sensors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;MotionSensor&lt;/span&gt; &lt;span class="n"&gt;accel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSensor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_ACCELEROMETER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accel&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;MotionSensorListener&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MotionSensorListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;motionReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MotionEvent&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getZ&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getParent&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;accel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manager itself is always available. Individual sensors return &lt;code&gt;null&lt;/code&gt; when the hardware is unsupported, so the app can keep one code path with a small capability check.&lt;/p&gt;

&lt;p&gt;Gestures are built on top of the sensor stream in the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addGestureListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;GestureEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_SHAKE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GestureListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;gestureDetected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GestureEvent&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shaken"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"You shook the device"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OK"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recognized set is practical rather than exotic: shake, flip face down, flip face up, tilt left, tilt right, tilt forward, tilt backward, pick up and free fall. Because recognition lives in &lt;code&gt;GestureEngine&lt;/code&gt;, every port that exposes an accelerometer gets the same behavior.&lt;/p&gt;

&lt;p&gt;Sampling is reference counted. The hardware powers on while listeners exist and powers down when they are removed. On iOS you still need &lt;code&gt;ios.NSMotionUsageDescription&lt;/code&gt; with a user-facing reason for motion access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rich Pointer Detail
&lt;/h2&gt;

&lt;p&gt;Classic pointer callbacks tell you where a press happened. Modern input asks more questions: was it a mouse, a finger, a stylus, or an eraser? Which button? What pressure? Was the pen tilted? Was the pointer hovering?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PointerEvent&lt;/code&gt; is the snapshot attached to pointer action events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;myComponent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPointerPressedListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSecondaryButton&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;showContextMenu&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;drawPreview&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPressure&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also poll the current dispatch state through &lt;code&gt;CN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerButton&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerPressure&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;pen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isStylusPointer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrentPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On plain touch screens these values have safe defaults: primary button, pressure &lt;code&gt;1.0&lt;/code&gt;, touch or unknown pointer type. Existing apps keep working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wheel, Trackpad And Context Menus
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WheelEvent&lt;/code&gt; is now the cross-platform scroll-gesture event. It handles mouse wheels, precision trackpads, horizontal scrolling, iOS/iPadOS pointer devices and the Apple Watch Digital Crown through the same path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addMouseWheelListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;WheelEvent&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WheelEvent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isControlDown&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDeltaY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consuming the event suppresses the default scroll. That is the difference between "wheel scrolls the container" and "control plus wheel zooms a design surface."&lt;/p&gt;

&lt;p&gt;For context menus, use the higher-level listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addContextMenuListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;showMyContextMenu&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fires for right click, stylus barrel button and long press, which is exactly the kind of cross-platform behavior app code should not have to rediscover.&lt;/p&gt;

&lt;p&gt;Trackpad magnify and rotate gestures route to component methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Container&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;pinch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;rotation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stylus And Foldables
&lt;/h2&gt;

&lt;p&gt;Stylus support exposes pressure, tilt, contact size and eraser type. A drawing surface can listen only to stylus events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;drawingArea&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addStylusListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPressure&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEraser&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;erase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;drawPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foldables use &lt;code&gt;DevicePosture&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;DevicePosture&lt;/span&gt; &lt;span class="n"&gt;posture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDevicePosture&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFoldable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isTableTop&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;layoutForTableTop&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFoldBounds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostureListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;relayoutForPosture&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDevicePosture&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Android, foldable support is opt-in with &lt;code&gt;android.foldableSupport=true&lt;/code&gt;, so apps that do not need the AndroidX window library do not carry it. The simulator can generate posture changes for testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Input used to mean key codes, pointer coordinates, and maybe a scroll wheel. That model is too small now. A modern app has to reason about pointer type, pressure, hover, buttons, tilt, posture, trackpads, sensor sampling, and gestures that may or may not exist on the current device.&lt;/p&gt;

&lt;p&gt;The new APIs do not make that complexity disappear. They put it behind capability checks and default-safe events, so the same component can handle a finger on a phone, a stylus on a tablet, a mouse on desktop, and a foldable hinge without turning every screen into platform-specific code.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>26 Repos in 29 Days With an AI Pipeline: What Actually Broke</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:20:35 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/26-repos-in-29-days-with-an-ai-pipeline-what-actually-broke-4jlm</link>
      <guid>https://dev.to/lucian_lkb_1f009d/26-repos-in-29-days-with-an-ai-pipeline-what-actually-broke-4jlm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This was originally published on &lt;a href="https://lkforge.com/blog/26-repos-in-29-days/" rel="noopener noreferrer"&gt;the LK Forge blog&lt;/a&gt;, where the commit chart is interactive and you can play the AI games it talks about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Between June 5 and July 3, 2026, I took an empty domain to &lt;strong&gt;26 repositories, 1,549 commits, and 335 live pages&lt;/strong&gt; — one developer, working with Claude Code. The interesting part isn't the volume. It's &lt;em&gt;which&lt;/em&gt; failure modes showed up, because none of them were the ones the AI-coding debate argues about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repositories&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commits&lt;/td&gt;
&lt;td&gt;1,549&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages shipped&lt;/td&gt;
&lt;td&gt;335&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Commit cadence: &lt;strong&gt;~53/day average&lt;/strong&gt;, &lt;strong&gt;peak 122 on June 20&lt;/strong&gt;, exactly &lt;strong&gt;one&lt;/strong&gt; zero-commit day in the sprint. The two heaviest days at the tail (114 each) were a site-wide URL-structure migration — which is itself one of the failure stories below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Deliberately boring: one developer, Claude Code in the terminal, git for everything, static-first architecture. Games and tools are self-contained repos of vanilla HTML/CSS/JS; the hub site is Astro. Everything deploys to Cloudflare Workers with static assets at the edge — no backend, no database, nothing to babysit at 3am.&lt;/p&gt;

&lt;p&gt;Every session ran the same loop: describe the goal, let the model plan and build, review the diff, make it verify its own work against the live site, commit. That verification step is what earns its keep — every failure below was caught by a check, not by luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pipeline was genuinely good at
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Working classical-AI engines, first try or close.&lt;/strong&gt; The 2048 solver is real expectimax search with a corner-snake heuristic and adaptive depth. Before publishing any performance number, I ran the exact production code through 250 headless self-play games: it reaches the 2048 tile in &lt;strong&gt;69.6% of games at ~0.5ms per move&lt;/strong&gt;. The tic-tac-toe opponent is minimax with alpha-beta; the pathfinding in Color Lines is BFS. Textbook algorithms, correctly implemented, shipped in days. That part of the hype is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume with consistency.&lt;/strong&gt; 335 pages sharing one brand system, one URL convention, one schema pattern. Once a convention was written into a project memory file, the model applied it across dozens of pages without drift. Those memory files turned out to be the highest-leverage artifact in the whole pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audits at a depth a human won't sustain.&lt;/strong&gt; Full link-graph crawls, redirect-chain verification across hundreds of URLs, per-page canonical checks against live HTTP. The model does the 400-URL tedium without getting bored — which matters, because tedium is where site-wide bugs hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually broke
&lt;/h2&gt;

&lt;p&gt;Not one failure was a syntax error, a broken build, or code that didn't run. Every real problem was &lt;strong&gt;structural&lt;/strong&gt; — invisible in any single diff, only visible when you look at the whole system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The pipeline competed with itself
&lt;/h3&gt;

&lt;p&gt;Asked for a word-tools hub, the pipeline built one — at &lt;code&gt;/word-tools/&lt;/code&gt;, while the existing tools lived under &lt;code&gt;/tools/&lt;/code&gt;. Two pages on the same domain targeting the same queries: textbook SEO cannibalization, self-inflicted. Search Console showed both URLs impressing for the same terms before I consolidated with a 301. Each page was locally correct; nobody was watching the query-level picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: the model optimizes the page you asked for, not the site you already have.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Twenty-six repos, two URL conventions, weeks of cleanup
&lt;/h3&gt;

&lt;p&gt;Some tools were built as flat files (&lt;code&gt;page.html&lt;/code&gt;), others as directories (&lt;code&gt;page/index.html&lt;/code&gt;). On Cloudflare's asset serving those get opposite trailing-slash behavior — so the site accumulated canonical mismatches, two-hop redirect chains, and Search Console redirect errors. The fix consumed the two biggest commit days of the sprint and produced a written URL convention plus a pre-deploy crawl checker that now gates every release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: conventions the model must follow have to be written before repo #2, not after repo #20.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Source and production drifted apart silently
&lt;/h3&gt;

&lt;p&gt;Game repos are mirrored into the hub site for deployment. Over weeks, SEO improvements were applied to the production mirror and never back-ported to source. The trap armed itself: the obvious "sync" — copy source over mirror — would have silently destroyed live metadata. It was caught only because a diff-before-copy check is now mandatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: any two copies of the same file will diverge, and the AI won't notice unless a check forces the comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The verification tools lied too
&lt;/h3&gt;

&lt;p&gt;The first link-graph audit reported a wave of orphaned pages. False alarm: it compared absolute URLs against unresolved relative hrefs. A later canonical audit reported 123 mismatches — also false, because the checker assumed file paths equal serving paths, and the CDN serves clean URLs. In both cases the &lt;em&gt;audit tooling&lt;/em&gt; — also AI-written — had the bug, and acting on its output would have "fixed" a healthy site into a broken one. Both caught the same way: probe the live site before believing static analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: verify the verifier. An AI-written check inherits every blind spot of the AI that wrote it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics: 93% of the cost was re-reading, not writing
&lt;/h2&gt;

&lt;p&gt;The sprint produced 44 working sessions and 826MB of transcripts. When I audited the token bill, the headline wasn't generation cost: &lt;strong&gt;roughly 93% of token consumption was cached context being re-read&lt;/strong&gt;, turn after turn, inside marathon sessions that should have been split up.&lt;/p&gt;

&lt;p&gt;The mechanics are mundane. A long session accumulates giant context; every subsequent turn re-reads it; a session that drifts across three unrelated tasks pays the full history of tasks one and two as a tax on task three. The model never complains, so nothing forces you to notice.&lt;/p&gt;

&lt;p&gt;The fix cost nothing: clear context between tasks, keep durable knowledge in small memory files, treat "one session = one task" as the default. If you run an AI coding workflow and have never audited where the tokens actually go, that single check is probably worth more than any prompt engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it earned (honest version)
&lt;/h2&gt;

&lt;p&gt;Over the last 28 days the site drew &lt;strong&gt;207 clicks from 7,320 impressions&lt;/strong&gt; across 257 pages with search data. For a domain about five weeks old, that's a normal, healthy trajectory — and nobody's growth-hack screenshot.&lt;/p&gt;

&lt;p&gt;The detail worth reporting: the single biggest click-earner after the homepage is the 2048 game with the visible AI solver — the page where the most genuine engineering lives. Search demand followed the depth, not the page count. 300 thin pages didn't beat one page with something real on it.&lt;/p&gt;

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

&lt;p&gt;Every one exists because its absence caused a real incident above. That's the only rule-making process that works.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark before you publish a number.&lt;/strong&gt; If it isn't measured, it doesn't ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff before you copy.&lt;/strong&gt; No exceptions for "they should be identical."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probe live before believing static analysis.&lt;/strong&gt; An audit result is a hypothesis until production confirms it over HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write conventions down before scaling them&lt;/strong&gt; — in a memory file the model loads every session, not in your head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One session, one task.&lt;/strong&gt; Context is the real cost center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the whole site, not the diff.&lt;/strong&gt; Cannibalization, drift, and convention splits are invisible at diff level.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;The live version, the interactive commit chart, and the AI games are at &lt;a href="https://lkforge.com/blog/26-repos-in-29-days/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;. The 2048 solver writeup with the full benchmark is &lt;a href="https://lkforge.com/games/2048/blog-how-the-ai-solver-works" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>We Built a Live Face Swap Preview in the Browser</title>
      <dc:creator>mix</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:17:58 +0000</pubDate>
      <link>https://dev.to/mixcbaea5/we-built-a-live-face-swap-preview-in-the-browser-jn9</link>
      <guid>https://dev.to/mixcbaea5/we-built-a-live-face-swap-preview-in-the-browser-jn9</guid>
      <description>&lt;p&gt;We built Vivify's Live Face Swap to transform a webcam feed while the session is running.&lt;/p&gt;

&lt;p&gt;Unlike an upload-and-wait face swap, it responds to movement and expression in the browser. The most interesting work was keeping access short-lived, metering an open-ended session, and deciding what the first release should not do.&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%2F6wtlg9rqwxk7onfe5jdl.jpg" 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%2F6wtlg9rqwxk7onfe5jdl.jpg" alt="Vivify live face swap browser preview showing camera input and AI output side by side" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can try in the browser
&lt;/h2&gt;

&lt;p&gt;Upload a reference image and press Start. The transformed stream appears beside the camera feed, so movement and expression are visible without rendering a clip first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two design challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session access:&lt;/strong&gt; the server issues a short-lived credential when the preview starts, keeping the reusable provider key off the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-ended usage:&lt;/strong&gt; unlike a normal generation job, a live session has no fixed duration. We meter active time with heartbeats, settle the final partial segment when it ends, and show usage while it runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the first release stops
&lt;/h2&gt;

&lt;p&gt;The browser version is preview-only: it does not record, export, or save the transformed stream. We also ask users to use only their own face or images they have permission to use.&lt;/p&gt;

&lt;p&gt;Try &lt;a href="https://vivify.video/realtime-face-swap" rel="noopener noreferrer"&gt;live face swap browser preview&lt;/a&gt; on Vivify. What would you test first: identity consistency, latency, expression preservation, or recovery after occlusion?&lt;/p&gt;

</description>
      <category>livefaceswap</category>
      <category>faceswap</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Closed World Innovation: Stop Telling People to "Think Outside the Box." There Is No Box.</title>
      <dc:creator>Tawanda Nyahuye</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:14:21 +0000</pubDate>
      <link>https://dev.to/towernter/closed-world-innovation-stop-telling-people-to-think-outside-the-box-there-is-no-box-50bk</link>
      <guid>https://dev.to/towernter/closed-world-innovation-stop-telling-people-to-think-outside-the-box-there-is-no-box-50bk</guid>
      <description>&lt;p&gt;"Think outside the box!"&lt;/p&gt;

&lt;p&gt;Great advice. Incredibly helpful. One small problem: nobody knows where the box is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What even is creativity, anyway?
&lt;/h2&gt;

&lt;p&gt;Before we get to boxes and closed worlds and all that fun stuff, let's settle on what creativity actually means, because "be creative" is about as actionable as "be taller."&lt;/p&gt;

&lt;p&gt;Creativity is when two things are both high at the same time: &lt;strong&gt;originality&lt;/strong&gt; and &lt;strong&gt;usefulness&lt;/strong&gt;. That's it. Something no one's ever seen before &lt;em&gt;and&lt;/em&gt; something that actually works. Just weird? Not creative. Just useful? Also, not creative, that's just engineering. Both? That's the good stuff.&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%2Fr7kycwdb0bagm22bzzh7.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%2Fr7kycwdb0bagm22bzzh7.png" alt=" " width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also simplicity in the mix, but let's not get greedy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "think outside the box" is terrible advice
&lt;/h2&gt;

&lt;p&gt;Here's what actually happens when you tell someone to think outside the box.&lt;/p&gt;

&lt;p&gt;They imagine a box. Then they try to think around it. And because the box they imagined is shaped by the exact same problem they're trying to solve, they end up either with solutions so bizarre they'd never survive contact with reality, or they circle all the way back to the traditional solution they started with.&lt;/p&gt;

&lt;p&gt;You've just run a very tiring mental exercise to arrive at the answer you already had. Congratulations.&lt;/p&gt;

&lt;p&gt;The phrase also has another problem: in the real world, there are no boxes. No one drew a boundary around your problem and labeled it "the box." The "box" is just a metaphor that your brain takes too literally and then gets confused by.&lt;/p&gt;

&lt;p&gt;The correct term, the one that actually helps, is the &lt;strong&gt;Closed World&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what's the Closed World?
&lt;/h2&gt;

&lt;p&gt;The Closed World was discovered by a researcher named Roni Horowitz, and the idea is almost annoyingly counterintuitive:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ideas are more creative when they are &lt;em&gt;confined&lt;/em&gt; to what's already around you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not outside. Not somewhere exotic. Right there. The resources, people, components, and constraints you already have are your closed world. And here's the punchline: constraints don't kill creativity. They &lt;em&gt;enhance&lt;/em&gt; it.&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%2Fz2p2j56w8i8rjckwdnmf.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%2Fz2p2j56w8i8rjckwdnmf.png" alt=" " width="799" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think about soldiers frying eggs on a shovel over a campfire because there's no pan. Or a forklift being used as an improvised cherry picker because there's no ladder tall enough. Nobody went outside their world to solve those problems. They looked at what they already had and asked: &lt;em&gt;what else can this do?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the closed world doing its thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why constraints are actually your friends
&lt;/h2&gt;

&lt;p&gt;You'd think unlimited freedom would produce unlimited creativity. That sounds logical. It is also completely wrong.&lt;/p&gt;

&lt;p&gt;When you have too many options, your brain does the creative equivalent of standing in front of a massive restaurant menu for twenty minutes and ordering a burger. You default to the familiar because the familiar is safe, and you're overwhelmed.&lt;/p&gt;

&lt;p&gt;Constraints force your brain to actually work. When you can't reach outside the problem, you start looking at the problem differently. You start noticing things about what you &lt;em&gt;have&lt;/em&gt; that you'd ignored before. Sometimes the solution was sitting right there the whole time, like the answer to a riddle you'd been overthinking.&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%2Fj43iwtd1pdosrs82b5zy.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%2Fj43iwtd1pdosrs82b5zy.png" alt=" " width="750" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing traditional thinking gets wrong about problems
&lt;/h2&gt;

&lt;p&gt;Traditional thinking says: problems are bad, solutions are good. Get rid of the problem as fast as possible and replace it with a solution.&lt;/p&gt;

&lt;p&gt;This sounds sensible. It is also why so many solutions are completely forgettable.&lt;/p&gt;

&lt;p&gt;If you sprint away from a problem, you never actually understand it. And if you don't understand it, you can't use it. Because here's the thing: &lt;strong&gt;in many cases, the problem itself is part of the creative solution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problem contains information. It contains context. It contains constraints. And those constraints, if you work within them instead of around them, are exactly what will lead you to something original and useful, you know, the two things that make something creative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apollo 13, the ultimate closed world story
&lt;/h2&gt;

&lt;p&gt;If you want to see closed world thinking in action at its most dramatic, look up the Apollo 13 CO2 filter problem. The astronauts had square filter cartridges. The ship needed round ones. People were going to die.&lt;/p&gt;

&lt;p&gt;The engineers at NASA didn't order new parts. They couldn't. They couldn't go outside the closed world; they were literally 200,000 miles from the nearest hardware store.&lt;/p&gt;

&lt;p&gt;So they sat down with exactly what was on that spacecraft and figured out how to fit a square peg in a round hole. And they did.&lt;/p&gt;

&lt;p&gt;That's not thinking outside the box. That's thinking &lt;em&gt;inside&lt;/em&gt; the only world available, and doing it brilliantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do you actually do with this?
&lt;/h2&gt;

&lt;p&gt;Next time you're stuck on a problem, instead of trying to escape it, do this:&lt;/p&gt;

&lt;p&gt;Draw your closed world. What people, tools, components, and resources are already part of this situation? What's right in front of you that you haven't thought to repurpose? What does the problem itself tell you about the solution?&lt;/p&gt;

&lt;p&gt;Then stay there. Don't go looking for something exotic. The opportunity is usually right under your nose; you've just been too busy looking for a box to climb out of.&lt;/p&gt;

&lt;p&gt;Stop thinking outside the box. Start thinking inside the closed world. The answers you need are probably already in the room.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>learning</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Fervor: Turn Your Passion into a Personalized Learning Roadmap with Google Gemini AI</title>
      <dc:creator>Abubakar umar</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:08:03 +0000</pubDate>
      <link>https://dev.to/sadeeq64/fervor-turn-your-passion-into-a-personalized-learning-roadmap-with-google-gemini-ai-4ekm</link>
      <guid>https://dev.to/sadeeq64/fervor-turn-your-passion-into-a-personalized-learning-roadmap-with-google-gemini-ai-4ekm</guid>
      <description>&lt;p&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fervor – Turn Your Passion into a Personalized Learning Roadmap with AI&lt;/p&gt;

&lt;p&gt;What I Built&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%2Fcw7t0jri7agf49j2y9gl.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%2Fcw7t0jri7agf49j2y9gl.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fervor is an AI-powered web application that helps people transform their passion into a clear, personalized learning roadmap.&lt;/p&gt;

&lt;p&gt;Many people know what they want to learn but don't know where to start. Whether it's web development, graphic design, data science, or any other skill, the biggest challenge is creating a structured learning path.&lt;/p&gt;

&lt;p&gt;Fervor solves this problem by asking users about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their passion&lt;/li&gt;
&lt;li&gt;Their learning goal&lt;/li&gt;
&lt;li&gt;Current experience level&lt;/li&gt;
&lt;li&gt;Motivation&lt;/li&gt;
&lt;li&gt;Weekly study time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Google Gemini AI, Fervor generates a personalized roadmap that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clear learning goal&lt;/li&gt;
&lt;li&gt;Estimated learning duration&lt;/li&gt;
&lt;li&gt;Three progressive learning stages&lt;/li&gt;
&lt;li&gt;Actionable tasks for each stage&lt;/li&gt;
&lt;li&gt;Daily motivational advice&lt;/li&gt;
&lt;li&gt;Progress tracking&lt;/li&gt;
&lt;li&gt;Local storage so users can continue where they left off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to create an application that feels like having a personal AI learning coach.&lt;/p&gt;

&lt;p&gt;Demo&lt;/p&gt;

&lt;p&gt;Live Demo&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://fervor-ai.vercel.app/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;fervor-ai.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;GitHub Repository&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sadikumar6413" rel="noopener noreferrer"&gt;
        sadikumar6413
      &lt;/a&gt; / &lt;a href="https://github.com/sadikumar6413/Fervor" rel="noopener noreferrer"&gt;
        Fervor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Fervor is an AI-powered web application that transforms your passion into a personalized learning roadmap using Google Gemini AI.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The complete source code is available on GitHub:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sadikumar6413" rel="noopener noreferrer"&gt;
        sadikumar6413
      &lt;/a&gt; / &lt;a href="https://github.com/sadikumar6413/Fervor" rel="noopener noreferrer"&gt;
        Fervor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Fervor is an AI-powered web application that transforms your passion into a personalized learning roadmap using Google Gemini AI.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;How I Built It&lt;/p&gt;

&lt;p&gt;Fervor was built using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML5&lt;/li&gt;
&lt;li&gt;CSS3&lt;/li&gt;
&lt;li&gt;Vanilla JavaScript&lt;/li&gt;
&lt;li&gt;Google Gemini API&lt;/li&gt;
&lt;li&gt;Local Storage&lt;/li&gt;
&lt;li&gt;Git &amp;amp; GitHub&lt;/li&gt;
&lt;li&gt;Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the biggest challenges was integrating the Google Gemini API.&lt;/p&gt;

&lt;p&gt;During development I encountered several issues, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect API configuration&lt;/li&gt;
&lt;li&gt;JavaScript bugs preventing roadmap generation&lt;/li&gt;
&lt;li&gt;API request errors&lt;/li&gt;
&lt;li&gt;HTTP 429 rate limit responses&lt;/li&gt;
&lt;li&gt;JSON parsing challenges when processing AI responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Debugging these problems taught me a lot about working with REST APIs, asynchronous JavaScript, error handling, and building resilient user experiences.&lt;/p&gt;

&lt;p&gt;Once the AI integration was working, I focused on creating a clean interface that keeps users focused on their learning journey instead of overwhelming them with information.&lt;/p&gt;

&lt;p&gt;Every generated roadmap is personalized based on the user's goals, making each experience unique.&lt;/p&gt;

&lt;p&gt;✅ Best Use of Google AI&lt;/p&gt;

&lt;p&gt;Google Gemini powers the heart of Fervor.&lt;/p&gt;

&lt;p&gt;It analyzes the user's interests, experience level, motivation, and available study time to generate a structured, personalized learning roadmap instead of returning generic advice.&lt;/p&gt;

&lt;p&gt;The AI creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Personalized learning paths&lt;/li&gt;
&lt;li&gt;Progressive milestones&lt;/li&gt;
&lt;li&gt;Actionable learning tasks&lt;/li&gt;
&lt;li&gt;Estimated timelines&lt;/li&gt;
&lt;li&gt;Motivational coaching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google AI transformed what would have been a static checklist into a dynamic and personalized learning experience.&lt;/p&gt;

&lt;p&gt;What I Learned&lt;/p&gt;

&lt;p&gt;This project reminded me that software development is much more than writing code.&lt;/p&gt;

&lt;p&gt;Building Fervor challenged me to think about user experience, AI integration, debugging, and resilience.&lt;/p&gt;

&lt;p&gt;Every bug solved taught me something new, and every successful AI response felt like another milestone in my journey as a developer.&lt;/p&gt;

&lt;p&gt;As a Computer Science student, this project pushed me beyond my comfort zone and gave me hands-on experience building and deploying a real AI-powered application.&lt;/p&gt;

&lt;p&gt;I'm excited to continue improving Fervor by adding secure backend integration, user accounts, cloud synchronization, and more intelligent AI coaching.&lt;/p&gt;

&lt;p&gt;Thank you for checking out my project! &lt;/p&gt;

&lt;h1&gt;
  
  
  weekendchallenge
&lt;/h1&gt;

</description>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>One Header, Full Admin: Attackers Exploit Critical Auth Bypass in Gitea Docker Images</title>
      <dc:creator>Etairos.ai</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:06:16 +0000</pubDate>
      <link>https://dev.to/etairos/one-header-full-admin-attackers-exploit-critical-auth-bypass-in-gitea-docker-images-38ek</link>
      <guid>https://dev.to/etairos/one-header-full-admin-attackers-exploit-critical-auth-bypass-in-gitea-docker-images-38ek</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;what:&lt;/strong&gt; CVE-2026-20896, a critical authentication bypass in the default configuration of official Gitea Docker images through 1.26.2, is under active exploitation via spoofed X-WEBAUTH-USER reverse-proxy headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;impact:&lt;/strong&gt; Attackers can impersonate any user, including administrators, with no password or token, giving them full control of source code, CI secrets, and everything downstream of the compromised Git server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fix:&lt;/strong&gt; Upgrade to Gitea 1.26.4 (1.26.3 fixed the flaw but introduced a regression) and lock REVERSE_PROXY_TRUSTED_PROXIES to explicit trusted IPs instead of a wildcard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;who:&lt;/strong&gt; Anyone running the official Gitea Docker image, especially the roughly 6,200 instances reachable from the public internet, and any org whose CI/CD pipeline trusts that Gitea as a source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No password. No token. One header. That is the entire exploitation story for CVE-2026-20896, a critical authentication bypass in the official Gitea Docker images that attackers began exploiting in the wild just 13 days after the advisory went public. If you run Gitea from the official container image at version 1.26.2 or earlier, an unauthenticated attacker who can reach your instance may be able to log in as any user on the system, including your administrators, by adding a single HTTP header to their request.&lt;/p&gt;

&lt;p&gt;The exposure is not theoretical. Roughly 6,200 Gitea instances are reachable on the public internet right now. Not all of them are confirmed vulnerable, but scanning is already underway: Sysdig security researcher Michael Clark observed a VPN-exit scanner probing specifically for this flaw, and Singapore's Cybersecurity Agency has issued its own alert urging immediate patching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flaw: trusting a header anyone can send
&lt;/h2&gt;

&lt;p&gt;Gitea, like many self-hosted services, supports reverse-proxy authentication. In that model, an upstream proxy such as nginx or an SSO gateway authenticates the user, then forwards the request to Gitea with an identity header, X-WEBAUTH-USER, telling Gitea who the user is. Gitea trusts that header and creates a session for the named account. The whole scheme depends on one assumption: only the trusted proxy can set that header.&lt;/p&gt;

&lt;p&gt;In the affected Docker images, the default configuration breaks that assumption. Deployments with reverse-proxy authentication headers enabled will accept X-WEBAUTH-USER from sources that were never supposed to be trusted. An attacker sends a request with X-WEBAUTH-USER set to an admin username, and Gitea obligingly h ands them that admin's session. No credential check, no second factor, no token. The header is the authentication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Full admin impersonation with zero credentials&lt;/strong&gt; — Because the bypass lets an attacker specify any username, the ceiling is total compromise. Impersonate an administrator and you own the repositories, the webhooks, the deploy keys, the CI/CD secrets, and the ability to push malicious commits into everything downstream that trusts this Git server. Treat any exposed, unpatched instance as already breached until logs prove otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The exploitation timeline
&lt;/h2&gt;

&lt;p&gt;This is a genuine active-exploitation story, not a proof-of-concept warning. The advisory for CVE-2026-20896 was published, and first in-the-wild exploitation followed 13 days later, well inside the window most organizations need just to notice a new CVE, let alone deploy a patched container across their fleet. The takeaway for defenders is blunt: the patch gap here is measured in days, and the attacker cost is a single crafted request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch carefully: 1.26.3 is not enough
&lt;/h2&gt;

&lt;p&gt;Gitea shipped a fix in 1.26.3, but that release introduced a regression, and 1.26.4 is the version that resolves both the vulnerability and the regression. Do not stop at 1.26.3. Pull the official image at 1.26.4 or later, redeploy, and confirm the running container reports the patched version rather than assuming your orchestration picked up the new tag.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade the official Gitea Docker image to 1.26.4 or later; do not settle for 1.26.3, which carries a known regression.&lt;/li&gt;
&lt;li&gt;Set REVERSE_PROXY_TRUSTED_PROXIES to the explicit IP addresses of your real proxies. Never use a wildcard, which trusts every source.&lt;/li&gt;
&lt;li&gt;If you do not use reverse-proxy authentication, disable it entirely so X-WEBAUTH-USER is ignored.&lt;/li&gt;
&lt;li&gt;Restrict public exposure. A self-hosted Git server rarely needs to answer requests from the entire internet; put it behind VPN or IP allowlisting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Assume compromise and hunt for it
&lt;/h2&gt;

&lt;p&gt;Patching closes the door, but if your instance was exposed and unpatched during the exploitation window, you need to determine whether someone already walked through it. Review access logs for requests carrying an X-WEBAUTH-USER header from IPs that are not your legitimate proxy. Look for admin logins with no corresponding credential event, new or altered SSH and deploy keys, unexpected webhook changes, and repository pushes or org membership changes you cannot account for. Because the bypass leaves a valid-looking session, the header origin in your proxy and application logs is often the clearest tell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The bigger lesson: header trust is a systemic pattern&lt;/strong&gt; — X-WEBAUTH-USER, X-Forwarded-For, X-Remote-User and their cousins show up across countless self-hosted apps, and every one of them is only as safe as the network boundary that is supposed to strip client-supplied copies. Audit every service in your stack that authenticates on a trusted header, confirm the upstream proxy strips inbound copies of that header, and never leave the trusted-proxy list set to a wildcard default.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gitea is a supply-chain chokepoint: it holds your source, your build config, and the keys to your pipeline. An auth bypass that hands an attacker administrator access to that chokepoint is about as bad as self-hosted vulnerabilities get. With active scanning confirmed and roughly 6,200 instances exposed, the safe assumption is that unpatched, internet-facing Gitea is being found right now. Get to 1.26.4, lock down the trusted-proxy list, and pull your logs before you close the ticket.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://threat-intelligence.redeyesecurity.com/blog/gitea-docker-auth-bypass-cve-2026-20896-exploited-2026" rel="noopener noreferrer"&gt;RedEye Threat Intelligence&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>PassionProof — A Soulbound NFT for Consistent Open Source Contribution</title>
      <dc:creator>Lymah</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:02:12 +0000</pubDate>
      <link>https://dev.to/lymah/passionproof-a-soulbound-nft-for-consistent-open-source-contribution-667</link>
      <guid>https://dev.to/lymah/passionproof-a-soulbound-nft-for-consistent-open-source-contribution-667</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;PassionProof is a Solana program that mints a soulbound badge for a real contribution, then makes it permanently, provably non-transferable.&lt;br&gt;
Digital achievements are usually transferable, sellable, or detached from the person who earned them. I wanted to explore the opposite: what if recognition for real contribution couldn't be traded? PassionProof issues a Token-2022 badge that's locked to the wallet it was minted to, carrying an on-chain "reason" field recording exactly what it's for.&lt;/p&gt;

&lt;p&gt;My own path into this space started as a self-taught, career-shifted developer — I hold a Biochemistry degree, not a CS one, and the thing that proved to me (and eventually to others) that I belonged in this field was becoming a core contributor to &lt;a href="https://github.com/molybdenumsoftware/statix" rel="noopener noreferrer"&gt;statix&lt;/a&gt;, &lt;em&gt;a Nix linter now canonical in nixpkgs&lt;/em&gt;. That's the milestone I recorded in the badge below: awarded_for: "Merged first PR into statix". Passion isn't just saying you love something; it's showing up consistently for work that doesn't always get seen, and I wanted a way to make that kind of record permanent instead of just a line on a resume.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;



&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/FPsAtNdXRG8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The recording walks through the real, on-chain test run: creating the badge mint, minting it to a recipient, then attempting to transfer it. Captions are on the video, but the key moment is worth pulling out here directly, at the point of the transfer attempt, the program log reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Program log: Transfer is disabled for this mint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's Token-2022 itself refusing the transfer at the protocol level, not a check written into my own program.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program deployed to devnet:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://explorer.solana.com/address/HSSLcVQmCdCo8qt9UBMSAL9vbqpxYLkCLHoz74dgrBE1?cluster=devnet" rel="noopener noreferrer"&gt;HSSLcVQmCdCo8qt9UBMSAL9vbqpxYLkCLHoz74dgrBE1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real transactions on devnet:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create_badge_mint&lt;/code&gt;:&lt;a href="https://explorer.solana.com/tx/2JcbBHtyRpFCcrsNJ4jNChMy46EKCg73wtUfY2j3Qq4ADCzRWbGLvD5DWQsDMnn7WjZHwWefpCNCrrbjst94hGtq?cluster=devnet" rel="noopener noreferrer"&gt;tx/2JcbBHtyRpFCcrsNJ4jNChMy46EKCg73wtUfY2j3Qq4ADCzRWbGLvD5DWQsDMnn7WjZHwWefpCNCrrbjst94hGtq&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mint_badge&lt;/code&gt;:&lt;a href="https://explorer.solana.com/tx/3NAjTLLFPf1PxkVf1BpxmLio68o357FEcaTSBA9WBjVs1KK5xKkPieSJr4UmDYmqKrAFKW6Us3nfZNSeBezESGFi?cluster=devnet" rel="noopener noreferrer"&gt;tx/3NAjTLLFPf1PxkVf1BpxmLio68o357FEcaTSBA9WBjVs1KK5xKkPieSJr4UmDYmqKrAFKW6Us3nfZNSeBezESGFi&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&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%2F3e7wtayt2pl7hjhx1np2.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%2F3e7wtayt2pl7hjhx1np2.png" alt="the deployed program account, confirming it's live and&lt;br&gt;
executable on devnet:" width="800" height="400"&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%2Fb96cozxunpsgayomzn08.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%2Fb96cozxunpsgayomzn08.png" alt="the create_badge_mint transaction log, showing the real&lt;br&gt;
Token-2022 instructions executed: InitializeNonTransferableMint,&lt;br&gt;
MetadataPointerInstruction::Initialize, InitializeMint2, and three&lt;br&gt;
TokenMetadataInstruction::UpdateField calls writing the category,&lt;br&gt;
awarded_for, and recipient/issued fields — all ending in&lt;br&gt;
Program returned success:" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;




&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Lymah123" rel="noopener noreferrer"&gt;
        Lymah123
      &lt;/a&gt; / &lt;a href="https://github.com/Lymah123/passionproof" rel="noopener noreferrer"&gt;
        passionproof
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;PassionProof&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A soulbound Solana badge for consistent, meaningful contribution — starting with
open source. Built for the DEV Weekend Challenge: Passion Edition.&lt;/p&gt;
&lt;p&gt;Digital achievements are usually transferable, sellable, or detached from the
person who earned them. PassionProof explores the opposite idea: what if
recognition for real contribution couldn't be traded? Every badge is a
Token-2022 mint with Solana's &lt;strong&gt;NonTransferable&lt;/strong&gt; extension, permanently
locking it to the wallet that earned it — plus an on-chain &lt;strong&gt;reason&lt;/strong&gt; field
so the badge is a verifiable record of a specific milestone, not just a
picture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deployed and verified on devnet:&lt;/strong&gt;
&lt;a href="https://explorer.solana.com/address/HSSLcVQmCdCo8qt9UBMSAL9vbqpxYLkCLHoz74dgrBE1?cluster=devnet" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;HSSLcVQmCdCo8qt9UBMSAL9vbqpxYLkCLHoz74dgrBE1&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it uses&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token-2022 NonTransferable extension&lt;/strong&gt; — enforced at the protocol level
any &lt;code&gt;TransferChecked&lt;/code&gt; instruction against the badge fails on-chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token-2022 MetadataPointer + TokenMetadata extensions&lt;/strong&gt; — metadata lives
directly on the mint account, no external metadata program required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom metadata fields&lt;/strong&gt; (&lt;code&gt;category&lt;/code&gt;, &lt;code&gt;awarded_for&lt;/code&gt;, &lt;code&gt;recipient&lt;/code&gt;
&lt;code&gt;issued&lt;/code&gt;) — the "reason" a…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Lymah123/passionproof" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;PassionProof is an Anchor program built on &lt;strong&gt;Solana's Token-2022&lt;/strong&gt; program,&lt;br&gt;
using two of its extensions instead of a traditional NFT metadata standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NonTransferable extension&lt;/strong&gt; — enforced at the protocol level. Once a badge mint has this extension, any transfer instruction against a token account holding it is rejected by the Token-2022 program itself. This isn't a convention or a frontend restriction; it's consensus-level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MetadataPointer + TokenMetadata extensions&lt;/strong&gt; — metadata lives directly on the mint account. No external metadata program, no off-chain indexer dependency for the core facts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The program exposes two instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_badge_mint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateBadgeMint&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;awarded_for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;recipient_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;mint_badge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MintBadge&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create_badge_mint&lt;/code&gt; initializes the mint with both extensions, then writes custom metadata fields — &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;awarded_for&lt;/code&gt;, &lt;code&gt;recipient&lt;/code&gt;, and &lt;code&gt;issued&lt;/code&gt; — directly into the mint's TLV metadata. &lt;code&gt;mint_badge&lt;/code&gt; mints exactly one unit into the recipient's associated token account. Because the mint is NonTransferable, that token account is now permanently bound to that wallet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One honest scope note&lt;/strong&gt;: the &lt;code&gt;awarded_for&lt;/code&gt; field is self-attested by whoever holds mint authority; the program records what the contribution was, but doesn't independently verify it against, say, the GitHub API. The trust model is the same as a signed certificate: the record is permanent and tamper-proof once issued, but the issuing authority is what backs the claim.&lt;br&gt;
GitHub-verified minting is the natural next step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Challenges — most of my actual weekend went into infrastructure, not logic, which felt worth being honest about:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Getting a local Anchor/Rust toolchain working in WSL hit a wall when a transitive dependency required Cargo's &lt;code&gt;edition2024&lt;/code&gt; feature, and the Solana CLI's bundled platform-tools compiler was too old to support it. Combined with unreliable network conditions that kept timing out large toolchain downloads, I switched to &lt;strong&gt;Solana Playground&lt;/strong&gt; (a browser-based Anchor IDE) to build, deploy, and test — the right call for a tight weekend timeline.&lt;/li&gt;
&lt;li&gt;Token-2022's metadata extension requires manually funding rent as the account grows (each metadata field write resizes the account), and I initially wrote a &lt;code&gt;realloc&lt;/code&gt; call into my program that only the account's &lt;em&gt;owning&lt;/em&gt; program (Token-2022, not mine) was actually allowed to perform. Solana enforces account ownership boundaries strictly.&lt;/li&gt;
&lt;li&gt;Making sure my "transfer fails" test proved the right thing, my first few attempts failed for boring reasons (empty wallet, missing destination account) rather than the NonTransferable extension itself. It took a few iterations to isolate the actual protocol-level rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd build next:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub-verified minting, so &lt;code&gt;awarded_for&lt;/code&gt; becomes a verified fact instead of a self-attested string&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A minimal frontend&lt;/strong&gt;: connect wallet → "Mint PassionProof" → see the badge and its "Soulbound: Cannot Transfer" state&lt;/li&gt;
&lt;li&gt;Support for multiple contribution categories beyond open source&lt;/li&gt;
&lt;li&gt;A path from devnet to mainnet once the extension logic has more mileage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Submitting to &lt;strong&gt;Best Use of Solana&lt;/strong&gt;. PassionProof uses Solana's native Token-2022 program directly, the NonTransferable extension, the MetadataPointer extension, and the TokenMetadata interface with custom fields, with no external metadata program or Metaplex dependency.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>I Open-Sourced My AI Agent Framework: Agents With Character, Rules, and the Ability to Build Their Own Tools</title>
      <dc:creator>lawcontinue</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:50:11 +0000</pubDate>
      <link>https://dev.to/zhangzeyu/i-open-sourced-my-ai-agent-framework-agents-with-character-rules-and-the-ability-to-build-their-jaa</link>
      <guid>https://dev.to/zhangzeyu/i-open-sourced-my-ai-agent-framework-agents-with-character-rules-and-the-ability-to-build-their-jaa</guid>
      <description>&lt;p&gt;I'm a lawyer. I write contracts, not Python — except I also write Python, because the legal mindset turns out to be surprisingly useful for building AI agents that don't run wild.&lt;/p&gt;

&lt;p&gt;This is about &lt;a href="https://github.com/lawcontinue/opensymphony" rel="noopener noreferrer"&gt;OpenSymphony&lt;/a&gt;, an agent framework I built and open-sourced under MIT. It's on PyPI (&lt;code&gt;pip install opensymphony&lt;/code&gt;), has 395 passing tests, and runs on a Mac mini.&lt;/p&gt;

&lt;p&gt;The thesis is simple. Most agent frameworks solve orchestration — how to chain LLM calls together. Almost none solve the harder problem: &lt;strong&gt;who is the agent, what rules does it follow, and who stops it when it's wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You've used LangChain agents. They're good at calling tools in sequence. They're bad at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency.&lt;/strong&gt; The agent that was helpful yesterday is a different agent today. Prompt drift is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accountability.&lt;/strong&gt; When an agent makes a decision, there's no record of why. No precedent. No review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety.&lt;/strong&gt; You can add a system prompt that says "be careful," but nothing structurally prevents risky actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth.&lt;/strong&gt; Agents don't learn. They make the same mistakes repeatedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenSymphony addresses these with three architectural pillars: Soul, Governance, and Self-evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 1: Soul — Persistent Agent Identity
&lt;/h2&gt;

&lt;p&gt;An agent's personality isn't a prompt. It's a YAML file that compiles into behavioral constraints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# souls/my_agent.yaml&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my_agent&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MyAgent&lt;/span&gt;
&lt;span class="na"&gt;archetype&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Reviewer&lt;/span&gt;

&lt;span class="na"&gt;thinking_framework&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;You are a code reviewer focused on security and correctness.&lt;/span&gt;
  &lt;span class="s"&gt;Rules:&lt;/span&gt;
  &lt;span class="s"&gt;1. Flag any unvalidated user input&lt;/span&gt;
  &lt;span class="s"&gt;2. Check for race conditions in concurrent code&lt;/span&gt;
  &lt;span class="s"&gt;3. Prefer readability over cleverness&lt;/span&gt;

&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Security first&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Evidence-based review&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Constructive feedback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;thinking_framework&lt;/code&gt; is the operational logic. &lt;code&gt;values&lt;/code&gt; are ranking principles when priorities conflict. You can also define &lt;code&gt;veto conditions&lt;/code&gt; — hard stops that prevent certain actions entirely.&lt;/p&gt;

&lt;p&gt;This isn't prompt engineering. The Soul Compiler transforms YAML into a structured behavioral framework that persists across conversations and sessions. The agent doesn't forget who it is.&lt;/p&gt;

&lt;p&gt;There are 13 built-in Souls: &lt;code&gt;themis&lt;/code&gt; (legal reasoning), &lt;code&gt;athena&lt;/code&gt; (strategy), &lt;code&gt;crit&lt;/code&gt; (adversarial review), &lt;code&gt;shield&lt;/code&gt; (security), &lt;code&gt;code&lt;/code&gt; (implementation), &lt;code&gt;novelist&lt;/code&gt;, &lt;code&gt;screenwriter&lt;/code&gt;, and others. Each has a distinct thinking framework, value hierarchy, and veto conditions.&lt;/p&gt;

&lt;p&gt;The legal analogy: a Soul is like a professional code of conduct. A lawyer doesn't reinvent attorney-client privilege every time they take a case. It's baked into the framework. Same idea here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 2: Governance — Structural Decision-Making
&lt;/h2&gt;

&lt;p&gt;This is where the legal background matters most.&lt;/p&gt;

&lt;p&gt;In a law firm, decisions go through layers. Associates draft, senior partners review, risk committees flag conflicts, and precedents guide future calls. OpenSymphony does the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → Gateway → [Intent Bridge] → [Governance] → Runtime → Kernel → Response
                                        ↑
                              Voting / Precedent / Defense
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The governance layer has four mechanisms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VotingMechanism&lt;/strong&gt; — Multiple agents vote on decisions. Configurable majority rules and timeout. If you have three agents and two say "this action is safe," it proceeds. One dissent gets logged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PrecedentStore&lt;/strong&gt; — Past decisions become searchable precedents. When an agent faces a similar decision later, it retrieves relevant history. This is literally how common law works — stare decisis for AI agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DefenseLayer&lt;/strong&gt; — Every action gets classified as &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;risky&lt;/code&gt;, or &lt;code&gt;dangerous&lt;/code&gt;. Risky actions require additional review. Dangerous actions are blocked unless a human overrides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HITLManager&lt;/strong&gt; — Human-in-the-loop confirmation for high-risk operations. The agent proposes, a human disposes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opensymphony.kernel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SymphonyKernel&lt;/span&gt;

&lt;span class="n"&gt;kernel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SymphonyKernel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_souls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;souls/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The request passes through governance before execution
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Should we deploy this untested change to production?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you ask the &lt;code&gt;crit&lt;/code&gt; agent whether to deploy untested code, the DefenseLayer flags it as risky. The VotingMechanism can poll multiple agents. The PrecedentStore surfaces similar past decisions. The HITLManager can require your sign-off.&lt;/p&gt;

&lt;p&gt;No single agent makes irreversible decisions alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 3: Self-Evolution — Agents Build Their Own Tools
&lt;/h2&gt;

&lt;p&gt;The Tool Workshop lets agents create, test, and deploy Python tools at runtime.&lt;/p&gt;

&lt;p&gt;An agent encounters a task it can't solve with existing tools. It writes a new one, tests it, and adds it to the registry. No human intervention required — but governance still applies. A new tool goes through the same risk classification as any other action.&lt;/p&gt;

&lt;p&gt;This matters because you can't predict every tool an agent will need. The alternative is either a massive hardcoded toolset (maintenance burden) or giving agents &lt;code&gt;eval&lt;/code&gt; access (security nightmare). The Workshop is a structured middle ground: agents can extend their capabilities, but within a sandbox with resource limits and governance oversight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: The Onion Model
&lt;/h2&gt;

&lt;p&gt;Every request passes through concentric layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│  Gateway (HTTP / WebSocket / CLI)                │
│    └─ HumanAdapter — Intent Bridge (NL→struct)   │
├─────────────────────────────────────────────────┤
│  Governance                                      │
│    ├─ VotingMechanism — multi-agent decisions     │
│    ├─ PrecedentStore — reusable past decisions    │
│    ├─ DefenseLayer — risk assessment              │
│    └─ HITLManager — human-in-the-loop             │
├─────────────────────────────────────────────────┤
│  Runtime                                         │
│    ├─ AgentPool — concurrent agent management     │
│    ├─ TaskScheduler — priority queue              │
│    └─ AgentSandbox — resource limits              │
├─────────────────────────────────────────────────┤
│  Kernel                                          │
│    ├─ Soul Compiler — YAML → behavioral rules     │
│    ├─ LLM Router — cloud + local providers        │
│    ├─ Memory (L1/L2/L3) — three-tier storage      │
│    └─ Tool Workshop — agents create tools         │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Intent Bridge translates natural language into structured intents. Governance reviews the intent. Runtime manages execution with resource limits. The Kernel handles the actual work: Soul compilation, LLM routing, memory, and tools.&lt;/p&gt;

&lt;p&gt;Nothing reaches the Kernel without passing through Governance first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-Tier Memory
&lt;/h2&gt;

&lt;p&gt;Agents need memory that works at different timescales:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;In-memory&lt;/td&gt;
&lt;td&gt;Current conversation context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Experience database with full-text search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;Cloud API&lt;/td&gt;
&lt;td&gt;Long-term persistent memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;L1 is fast and ephemeral. L2 is where agents store lessons learned — things like "this API rate-limits at 100 req/min" or "this user prefers terse responses." L3 is optional cloud storage for cross-session persistence.&lt;/p&gt;

&lt;p&gt;The PrecedentStore sits across L2 and L3. When an agent makes a decision, it's stored. When a similar situation arises, the precedent is retrieved. Over time, the system builds a case law of its own decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM Routing — Cloud and Local
&lt;/h2&gt;

&lt;p&gt;The LLM Router supports both cloud APIs and local models. You can route different agents to different models — a simple classification task might use a local 7B model, while complex reasoning goes to a cloud provider.&lt;/p&gt;

&lt;p&gt;This matters for cost and privacy. A governance system that sends every decision to a third-party API is a governance system with a data leak. Local models keep sensitive decisions on your hardware.&lt;/p&gt;

&lt;p&gt;Consumer hardware works. I develop and run OpenSymphony on a Mac mini with 16GB RAM. Local inference isn't fast, but it's functional.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 395 Tests Get You
&lt;/h2&gt;

&lt;p&gt;The test suite covers the governance layer, Soul compilation, memory tiers, the LLM router, and the tool workshop. Governance logic needs deterministic tests — you can't rely on LLM probability to verify that the DefenseLayer correctly blocks a dangerous action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lawcontinue/opensymphony.git
&lt;span class="nb"&gt;cd &lt;/span&gt;opensymphony
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
pytest  &lt;span class="c"&gt;# 395 passing, 6 known intent-bridge failures&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opensymphony
python &lt;span class="nt"&gt;-m&lt;/span&gt; opensymphony.gateway.http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define a Soul in YAML, load it, and start talking to your agent. The governance layer is optional — you can start without it and add voting, defense, or HITL as your use case demands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opensymphony.agents.soul&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Soul&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opensymphony.agents.soul_compiler&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;compile_soul&lt;/span&gt;

&lt;span class="n"&gt;soul&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Soul&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_yaml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;souls/my_agent.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compile_soul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why a Lawyer Built This
&lt;/h2&gt;

&lt;p&gt;Legal systems have spent ~900 years solving a problem that AI agents now face: how do you give autonomous actors decision-making power while preventing abuse?&lt;/p&gt;

&lt;p&gt;The answer in law is structural. Constitutions, precedent, voting procedures, defense counsel, appellate review. Not "please be careful" instructions in a system prompt.&lt;/p&gt;

&lt;p&gt;OpenSymphony borrows these patterns. Souls are codes of conduct. The PrecedentStore is case law. The DefenseLayer is risk assessment. VotingMechanism is deliberative democracy. HITLManager is judicial review.&lt;/p&gt;

&lt;p&gt;The goal isn't to make agents "ethical" through prompt engineering. It's to build structural constraints that make bad behavior difficult, auditable, and correctable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status and Roadmap
&lt;/h2&gt;

&lt;p&gt;The framework is MIT-licensed and on &lt;a href="https://github.com/lawcontinue/opensymphony" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The 13 built-in Souls cover use cases from code review to creative writing. Application modules exist for novel pipelines and content production.&lt;/p&gt;

&lt;p&gt;What's there now: Soul system, full governance layer, three-tier memory, LLM routing, tool workshop, HTTP/WebSocket/CLI gateways.&lt;/p&gt;

&lt;p&gt;What's next: more Souls, better local model integration, and expanding the precedent retrieval system with semantic search.&lt;/p&gt;

&lt;p&gt;If you build agent systems and have opinions about governance, I'd like to hear them. Issues and PRs are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Session Manager removes your public IPs. Is it enough for an audit?</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:49:54 +0000</pubDate>
      <link>https://dev.to/upstood/session-manager-removes-your-public-ips-is-it-enough-for-an-audit-bji</link>
      <guid>https://dev.to/upstood/session-manager-removes-your-public-ips-is-it-enough-for-an-audit-bji</guid>
      <description>&lt;p&gt;Every public IP on an EC2 instance exists for one of two reasons: the instance serves the internet, or somebody needs to SSH into it. The second reason stopped being valid years ago, and it is still the most common one we see.&lt;/p&gt;

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

&lt;p&gt;Instances get a public IP so an engineer can reach them over SSH. That single decision drags a chain of exposure behind it: a security group with port 22 open (often to 0.0.0.0/0 "temporarily"), key pairs shared over Slack and never rotated, an internet gateway route on subnets that never needed one, and a bastion host that is itself another public instance with another key pair.&lt;/p&gt;

&lt;p&gt;From an audit standpoint, every one of these is a finding. From an attacker's standpoint, every public IP is scanned within minutes of being assigned. And from an operations standpoint, none of it is necessary: AWS Systems Manager Session Manager gives you a shell on any instance with zero inbound ports, zero public IPs, and zero SSH keys.&lt;/p&gt;

&lt;p&gt;The biggest advantage deserves to be stated plainly: port 22 is not restricted, it is closed. Not narrowed to an office IP, not hidden behind a bastion: there is no listening port to protect at all, because access happens over the web. The shell opens in a browser tab from the AWS console (or from the CLI), authenticated by the IAM login the engineer already has.&lt;/p&gt;

&lt;p&gt;We have applied this in two very different settings, and the experience splits cleanly. In startups and small companies, Session Manager is a pure win: public IPs disappear, key management disappears, and engineers open a shell on any instance on the fly from the console or CLI. In enterprises, the network part is just as easy, but a different question stops the rollout: once everyone enters the VM through the same door, how do you prove who did what inside it? That question, and what to ship to the SIEM, is where most of this article lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;Session Manager inverts the connection model. The SSM Agent on the instance opens an outbound HTTPS connection to the Systems Manager service and waits. When you start a session, the service brokers it over that existing outbound channel. Nothing ever connects inbound to the instance, so there is nothing to expose. That inversion is what makes the browser shell possible: the operator's session rides the agent's outbound channel, which is why port 22 can stay closed on every instance in the fleet.&lt;/p&gt;

&lt;p&gt;Three things have to be true for this to work, and they are smaller than most teams expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The minimum instance role
&lt;/h3&gt;

&lt;p&gt;The instance needs an instance profile whose role allows the SSM Agent to talk to the service. The managed policy &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; is the supported baseline. If you want the actual minimum instead of the managed policy, this is what the agent needs for sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:UpdateInstanceInformation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateDataChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenDataChannel"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;s3:GetEncryptionConfiguration&lt;/code&gt; and &lt;code&gt;s3:PutObject&lt;/code&gt; on your logging bucket, or &lt;code&gt;logs:CreateLogStream&lt;/code&gt; and &lt;code&gt;logs:PutLogEvents&lt;/code&gt; on your logging group, if you enable session logging (you should, see below). Add &lt;code&gt;kms:GenerateDataKey&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; on the session KMS key if you encrypt sessions (you should as well).&lt;/p&gt;

&lt;p&gt;Note what is not in this policy: nothing. No inbound rule, no key material, no &lt;code&gt;ec2:*&lt;/code&gt;. The instance role is the entire server-side setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The minimum user policy
&lt;/h3&gt;

&lt;p&gt;On the operator side, scope &lt;code&gt;ssm:StartSession&lt;/code&gt; to the instances the person is allowed to reach. Tag-based scoping keeps this manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:StartSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ssm:resourceTag/Environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:StartSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:TerminateSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:ResumeSession"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:session/${aws:username}-*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session-document statement matters: without pinning the document, a user who can start any session can bypass your logging preferences by choosing a different session document.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The network path (the "firewall")
&lt;/h3&gt;

&lt;p&gt;The agent needs outbound HTTPS to three service endpoints: &lt;code&gt;ssm&lt;/code&gt;, &lt;code&gt;ssmmessages&lt;/code&gt;, and &lt;code&gt;ec2messages&lt;/code&gt;. In a private subnet with no NAT, that means three interface VPC endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.ssm
com.amazonaws.&amp;lt;region&amp;gt;.ssmmessages
com.amazonaws.&amp;lt;region&amp;gt;.ec2messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security group on the endpoints allows inbound 443 from the instance security groups (or the VPC CIDR). The instance security group needs outbound 443 to the endpoint security group and nothing else. No inbound rules at all. If session logs go to S3 or CloudWatch, add the &lt;code&gt;s3&lt;/code&gt; gateway endpoint and the &lt;code&gt;logs&lt;/code&gt; interface endpoint; add &lt;code&gt;kms&lt;/code&gt; if sessions are encrypted.&lt;/p&gt;

&lt;p&gt;If the subnets already have a NAT gateway, the endpoints are optional (the agent will happily use the regional public endpoints over NAT), but the interface endpoints keep session traffic off the NAT data-processing meter and inside the VPC.&lt;/p&gt;

&lt;p&gt;With those three pieces in place, the public IPs, the bastion, port 22, and the key pairs are all deletable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it, and when an auditor pushes back
&lt;/h2&gt;

&lt;p&gt;Session Manager is the right default for interactive administrative access to EC2. It is not automatically audit-clean. If we were reviewing your account, this is what we would check.&lt;/p&gt;

&lt;p&gt;Use it when access is interactive, occasional, and administrative: debugging, incident response, one-off inspection. Every session start is a &lt;code&gt;StartSession&lt;/code&gt; event in CloudTrail tied to an IAM principal, which is already better evidence than an SSH login against a shared key.&lt;/p&gt;

&lt;p&gt;Turn on session logging before you call it a control. Out of the box, Session Manager records that a session happened, not what happened inside it. Session activity logging to S3 or CloudWatch Logs (with KMS encryption on both the session and the destination) captures the full terminal transcript. Without it, an auditor will correctly note that your privileged access has no content trail.&lt;/p&gt;

&lt;p&gt;The identity gap: everyone is &lt;code&gt;ssm-user&lt;/code&gt;. This is the finding most teams miss, and in our experience it is the single point where enterprise rollouts stall. Session Manager authenticates the AWS principal, but on the operating system every session lands as the same local account, &lt;code&gt;ssm-user&lt;/code&gt;, with sudo rights by default. Your OS logs, auditd trail, and file ownership all say &lt;code&gt;ssm-user&lt;/code&gt; did it. If your access policy or your regulator expects OS-level actions to be attributable to a named individual (and most do), the default setup fails that requirement even though AWS knows exactly who started the session.&lt;/p&gt;

&lt;p&gt;What the console identity already gives you. Before reaching for workarounds, know what is attributable out of the box. When a user starts a session from the console or CLI, CloudTrail records the &lt;code&gt;StartSession&lt;/code&gt; event with their full principal ARN, and the session ID itself is generated with the IAM user name as its prefix. This is why AWS's own example policies can scope &lt;code&gt;ssm:TerminateSession&lt;/code&gt; to &lt;code&gt;arn:aws:ssm:*:*:session/${aws:username}-*&lt;/code&gt;. Since the CloudWatch log stream for the session transcript is named after the session ID, the transcript of everything typed and returned in the session is already tied to the console identity by name. What AWS does not do is carry that identity into the operating system: there is no mechanism that logs the console IAM user into the box as themselves, and no automatic provisioning of OS accounts from IAM identities. Inside the VM, attribution stops at &lt;code&gt;ssm-user&lt;/code&gt; unless you configure Run As.&lt;/p&gt;

&lt;p&gt;Workarounds for the OS-level gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run As. Session Manager preferences can run sessions as a named OS user instead of &lt;code&gt;ssm-user&lt;/code&gt;, taken from the &lt;code&gt;SSMSessionRunAs&lt;/code&gt; tag on the IAM principal. Three constraints to plan around: it is Linux and macOS only, the &lt;code&gt;root&lt;/code&gt; account is not supported, and once Run As is on there is no fallback to &lt;code&gt;ssm-user&lt;/code&gt;. A missing tag or missing OS account means no session. From an audit perspective the no-fallback behavior is a feature: nobody can silently drop back to the shared identity.&lt;/li&gt;
&lt;li&gt;Run As works per person even under SSO. The obvious objection in a federated setup (Entra ID or any SAML IdP in front of AWS) is that everyone shares the same role, so a tag on the role would map everyone to the same OS user. The answer is session tags: the IdP passes &lt;code&gt;SSMSessionRunAs&lt;/code&gt; as a SAML attribute (&lt;code&gt;https://aws.amazon.com/SAML/Attributes/AccessControl:SSMSessionRunAs&lt;/code&gt;) with a per-user value, and each person's role session carries their own OS user name. With IAM Identity Center, enable Attributes for Access Control and map the attribute to &lt;code&gt;${path:userName}&lt;/code&gt; so it flows from the directory automatically. Same permission set, individual OS identity per human.&lt;/li&gt;
&lt;li&gt;The OS account still has to exist first. Session Manager verifies the target account before starting the session and refuses the connection if it is missing. It never creates users. That leaves an enterprise with exactly two options: provision matching Linux accounts on every node ahead of time (user data, Ansible, or a State Manager association syncing the user list), or domain-join the instances so the Run As target is a domain user that exists everywhere by definition. The domain-join path is the cleanest where AD is already present, and OS-level attribution then matches the corporate identity, but be precise about what it is: Session Manager checks that the domain account exists and runs the session as it. The mapping from AWS principal to domain user is tag-based trust, not a Kerberos logon, and an auditor should understand it as such. Note for Entra ID shops: Entra ID alone is not a joinable domain in the classic sense; this path needs real Active Directory in the picture (AWS Managed Microsoft AD, or hybrid AD synced through Entra Connect).&lt;/li&gt;
&lt;li&gt;Accept correlation as the control for small teams: session transcripts carry the IAM identity in their name (see above), CloudTrail carries the principal, and OS logs carry &lt;code&gt;ssm-user&lt;/code&gt; plus timestamps. Documenting that correlation procedure is defensible for low-change environments and startups. It is weak for regulated ones, because it degrades exactly when you need it most: concurrent sessions on the same instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When OS logs need to reach the SIEM
&lt;/h2&gt;

&lt;p&gt;Session transcripts in S3 are evidence, but they are not detection. Ship OS-level logs to the SIEM when any of these hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulated workloads. If the instance is in scope for PCI DSS, ISO 27001 A.8.15/A.8.16, or similar, privileged session activity must be centrally collected and reviewed, not just stored. A transcript bucket nobody reads does not satisfy "monitored".&lt;/li&gt;
&lt;li&gt;Run As is your attribution control. Then &lt;code&gt;/var/log/secure&lt;/code&gt; and the auditd trail are where the named OS user actually appears, and they need to be tamper-evident and off-box. CloudWatch Agent to CloudWatch Logs, then a subscription filter to Kinesis Data Firehose into the SIEM, is the standard path and keeps the instance role additions minimal.&lt;/li&gt;
&lt;li&gt;You need alerting on what happens inside sessions, not just that they occurred: sudo to root, package installs, outbound connections initiated from an interactive shell. That is auditd or Sysmon territory, and it only has value if a detection pipeline consumes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of those apply (dev accounts, no compliance scope, low blast radius), CloudTrail plus encrypted session transcripts is a proportionate control, and the SIEM integration can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;p&gt;For a startup, the impact is immediate and unambiguous: public IPs deleted, port 22 closed everywhere, key pairs revoked, bastion terminated, and engineers reach any instance on the fly with nothing but their existing IAM login. The whole migration is an instance profile, three VPC endpoints, and one afternoon.&lt;/p&gt;

&lt;p&gt;For an enterprise, the network win is identical, but the honest impact statement includes the second phase: Run As backed by provisioned OS or domain accounts, session transcripts encrypted and centralized, and OS logs flowing to the SIEM where compliance scope demands it. That phase is real work. It is still less work than operating a bastion fleet with rotating SSH keys, and unlike the bastion, it ends with every privileged session attributable to a named person at both the AWS layer and the OS layer.&lt;/p&gt;

&lt;p&gt;The end state in both cases is the same: zero inbound ports for administrative access, and the answer to "why does this instance have a public IP" is only ever "it serves the internet".&lt;/p&gt;




&lt;p&gt;Want us to look for issues like this in your account? We offer a free AWS audit: &lt;a href="https://www.upstood.com" rel="noopener noreferrer"&gt;upstood.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>sessionmanager</category>
      <category>ssh</category>
    </item>
    <item>
      <title>Day 74/100 - Cross-Datacenter Replication in ClickHouse®</title>
      <dc:creator>Kanishga Subramani</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:46:41 +0000</pubDate>
      <link>https://dev.to/kanishga_subramani_49ad73/day-74100-cross-datacenter-replication-in-clickhouser-2kh1</link>
      <guid>https://dev.to/kanishga_subramani_49ad73/day-74100-cross-datacenter-replication-in-clickhouser-2kh1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Modern applications increasingly operate across multiple geographical regions to improve availability, reduce latency, and ensure business continuity. Whether you're building a disaster recovery strategy, enabling multi-region analytics, or serving users closer to their location, replicating data across data centers has become a critical architectural requirement.&lt;/p&gt;

&lt;p&gt;ClickHouse® supports cross-datacenter replication through its &lt;strong&gt;ReplicatedMergeTree&lt;/strong&gt; table engine. Combined with a distributed ClickHouse cluster and a coordination service such as &lt;strong&gt;ClickHouse Keeper&lt;/strong&gt;, it allows organizations to maintain synchronized copies of data across multiple data centers while continuing to deliver high-performance analytical queries.&lt;/p&gt;

&lt;p&gt;This architecture ensures that if one data center becomes unavailable due to hardware failures, network outages, or maintenance, other replicas continue serving requests with minimal disruption.&lt;/p&gt;

&lt;p&gt;In this article, you'll learn how cross-datacenter replication works in ClickHouse®, the components involved, how to configure a replicated cluster, monitor replication health, troubleshoot common issues, and follow best practices for reliable deployments.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Use Cross-Datacenter Replication?
&lt;/h1&gt;

&lt;p&gt;Replicating data across multiple data centers offers several operational and business advantages.&lt;/p&gt;

&lt;p&gt;Some of the primary benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High availability&lt;/li&gt;
&lt;li&gt;Disaster recovery&lt;/li&gt;
&lt;li&gt;Regional redundancy&lt;/li&gt;
&lt;li&gt;Business continuity&lt;/li&gt;
&lt;li&gt;Reduced query latency for geographically distributed users&lt;/li&gt;
&lt;li&gt;Improved fault tolerance&lt;/li&gt;
&lt;li&gt;Maintenance with minimal downtime&lt;/li&gt;
&lt;li&gt;Increased read scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cross-datacenter replication is commonly used in industries such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Financial services&lt;/li&gt;
&lt;li&gt;Telecommunications&lt;/li&gt;
&lt;li&gt;E-commerce&lt;/li&gt;
&lt;li&gt;Healthcare&lt;/li&gt;
&lt;li&gt;SaaS platforms&lt;/li&gt;
&lt;li&gt;Media and streaming services&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  How Replication Works in ClickHouse®
&lt;/h1&gt;

&lt;p&gt;A single-node ClickHouse deployment has one major limitation—if the server becomes unavailable, so does access to your data.&lt;/p&gt;

&lt;p&gt;Replication addresses this challenge by maintaining synchronized copies of data across multiple nodes.&lt;/p&gt;

&lt;p&gt;ClickHouse replication is powered by the &lt;strong&gt;ReplicatedMergeTree&lt;/strong&gt; table engine.&lt;/p&gt;

&lt;p&gt;Instead of copying entire tables repeatedly, ClickHouse replicates only the newly created data parts. Metadata about these parts is coordinated using &lt;strong&gt;ClickHouse Keeper&lt;/strong&gt;, while each replica maintains its own local copy of the data.&lt;/p&gt;

&lt;p&gt;This architecture minimizes network traffic while keeping replicas synchronized efficiently.&lt;/p&gt;

&lt;p&gt;Some key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fault tolerance—if one replica fails, others continue serving queries.&lt;/li&gt;
&lt;li&gt;High availability with no single point of failure.&lt;/li&gt;
&lt;li&gt;Read scalability by distributing SELECT queries across replicas.&lt;/li&gt;
&lt;li&gt;Zero-downtime maintenance by taking individual nodes offline without interrupting service.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Cluster Overview
&lt;/h1&gt;

&lt;p&gt;For this example, consider a cluster consisting of one shard with three replicas distributed across three different nodes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;IP Address&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node 1&lt;/td&gt;
&lt;td&gt;10.x.x.1&lt;/td&gt;
&lt;td&gt;Replica&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 2&lt;/td&gt;
&lt;td&gt;10.x.x.2&lt;/td&gt;
&lt;td&gt;Replica&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 3&lt;/td&gt;
&lt;td&gt;10.x.x.3&lt;/td&gt;
&lt;td&gt;Replica&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each node stores a complete copy of the data.&lt;/p&gt;

&lt;p&gt;If any single node becomes unavailable, the remaining replicas continue processing queries without data loss.&lt;/p&gt;

&lt;p&gt;The overall architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Application
                     │
                     ▼
            Distributed Table
                     │
     ┌───────────────┼───────────────┐
     ▼               ▼               ▼
Node 1          Node 2          Node 3
Replicated      Replicated      Replicated
MergeTree       MergeTree       MergeTree
     └───────────────┼───────────────┘
                     ▼
            ClickHouse® Keeper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Core Components
&lt;/h1&gt;

&lt;p&gt;Cross-datacenter replication relies on three primary components working together.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ReplicatedMergeTree
&lt;/h2&gt;

&lt;p&gt;This is the storage engine responsible for replication.&lt;/p&gt;

&lt;p&gt;Each replica stores its own copy of the table while automatically synchronizing newly inserted data with other replicas.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. ClickHouse Keeper
&lt;/h2&gt;

&lt;p&gt;ClickHouse Keeper is the coordination service responsible for managing replication metadata.&lt;/p&gt;

&lt;p&gt;It tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Available replicas&lt;/li&gt;
&lt;li&gt;Newly created data parts&lt;/li&gt;
&lt;li&gt;Replication queues&lt;/li&gt;
&lt;li&gt;Leader election&lt;/li&gt;
&lt;li&gt;Replica synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeper uses the &lt;strong&gt;Raft consensus algorithm&lt;/strong&gt; to ensure fault-tolerant coordination.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Distributed Table
&lt;/h2&gt;

&lt;p&gt;Applications should interact with a &lt;strong&gt;Distributed&lt;/strong&gt; table instead of local replicated tables.&lt;/p&gt;

&lt;p&gt;The Distributed table automatically routes queries and inserts to the appropriate replicas while hiding the underlying cluster topology.&lt;/p&gt;




&lt;h1&gt;
  
  
  Configuration Overview
&lt;/h1&gt;

&lt;p&gt;Setting up replication requires several configuration files under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/clickhouse-server/config.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file serves a different purpose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines shards and replicas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keeper.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configures ClickHouse Keeper and RAFT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zookeeper.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies Keeper endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macros.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines shard and replica identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After updating these configuration files, restart ClickHouse on every node before creating replicated tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cluster.xml&lt;/code&gt; is identical across all nodes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zookeeper.xml&lt;/code&gt; is identical across all nodes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keeper.xml&lt;/code&gt; contains node-specific &lt;code&gt;server_id&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;macros.xml&lt;/code&gt; contains node-specific replica names.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Creating Replicated Tables
&lt;/h1&gt;

&lt;p&gt;Once the cluster is configured and ClickHouse has been restarted on every node, create the database and tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Create the Database
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;cluster_1S_3R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — Create the Local Replicated Table
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_local&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;cluster_1S_3R&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/clickhouse/tables/{shard}/orders_local'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{shard}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{replica}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are automatically replaced using values from &lt;code&gt;macros.xml&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Create the Distributed Table
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_distributed&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;cluster_1S_3R&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_local&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cluster_1S_3R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;orders_local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rand&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;Applications should perform all inserts and queries using this Distributed table.&lt;/p&gt;




&lt;h1&gt;
  
  
  Verifying Replication
&lt;/h1&gt;

&lt;p&gt;Insert sample records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_distributed&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;450&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-02'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Charlie'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;890&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-03'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Diana'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;670&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-04'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now query the local table from each node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_local&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output on every replica:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;customer&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;th&gt;order_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;1200.00&lt;/td&gt;
&lt;td&gt;2024-01-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;450.00&lt;/td&gt;
&lt;td&gt;2024-01-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Charlie&lt;/td&gt;
&lt;td&gt;890.00&lt;/td&gt;
&lt;td&gt;2024-01-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Diana&lt;/td&gt;
&lt;td&gt;670.00&lt;/td&gt;
&lt;td&gt;2024-01-04&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seeing identical data across all replicas confirms that replication is functioning correctly.&lt;/p&gt;




&lt;h1&gt;
  
  
  How Replication Works Internally
&lt;/h1&gt;

&lt;p&gt;When an application inserts data into the Distributed table, ClickHouse performs the following sequence automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      │
      ▼
Distributed Table
      │
      ▼
Selected Replica
      │
      ▼
Writes Data Part
      │
      ▼
ClickHouse Keeper Updates Metadata
      │
      ▼
Remaining Replicas Fetch Data
      │
      ▼
Replication Complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process consists of these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Distributed table routes the insert to one replica.&lt;/li&gt;
&lt;li&gt;That replica writes a new data part locally.&lt;/li&gt;
&lt;li&gt;ClickHouse Keeper records the new data part.&lt;/li&gt;
&lt;li&gt;Other replicas detect the update.&lt;/li&gt;
&lt;li&gt;Replicas download the new part.&lt;/li&gt;
&lt;li&gt;Keeper confirms successful synchronization.&lt;/li&gt;
&lt;li&gt;Every replica now contains identical data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This synchronization occurs automatically without requiring manual intervention.&lt;/p&gt;




&lt;h1&gt;
  
  
  Monitoring Replication
&lt;/h1&gt;

&lt;p&gt;Monitoring replica health is essential for production clusters.&lt;/p&gt;

&lt;p&gt;Query the &lt;code&gt;system.replicas&lt;/code&gt; table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;replica_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_leader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_readonly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_replicas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;active_replicas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;queue_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replicas&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'orders_local'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;database&lt;/th&gt;
&lt;th&gt;table&lt;/th&gt;
&lt;th&gt;replica_name&lt;/th&gt;
&lt;th&gt;is_leader&lt;/th&gt;
&lt;th&gt;is_readonly&lt;/th&gt;
&lt;th&gt;total_replicas&lt;/th&gt;
&lt;th&gt;active_replicas&lt;/th&gt;
&lt;th&gt;queue_size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics&lt;/td&gt;
&lt;td&gt;orders_local&lt;/td&gt;
&lt;td&gt;replica_1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics&lt;/td&gt;
&lt;td&gt;orders_local&lt;/td&gt;
&lt;td&gt;replica_2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics&lt;/td&gt;
&lt;td&gt;orders_local&lt;/td&gt;
&lt;td&gt;replica_3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Healthy replication typically shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;active_replicas = total_replicas&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_readonly = 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue_size = 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Checking Replication Errors
&lt;/h1&gt;

&lt;p&gt;To identify replication failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;replica_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_exception&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replicas&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_exception&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any returned rows indicate replication issues that require investigation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Verifying ClickHouse Keeper
&lt;/h1&gt;

&lt;p&gt;You can verify Keeper availability using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ruok"&lt;/span&gt; | nc 10.x.x.1 9181
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected response:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If Keeper does not respond, replication will eventually stop because replicas can no longer coordinate metadata.&lt;/p&gt;




&lt;h1&gt;
  
  
  Common Issues and Solutions
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Recommended Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Replica becomes read-only&lt;/td&gt;
&lt;td&gt;Keeper unavailable&lt;/td&gt;
&lt;td&gt;Verify Keeper connectivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication queue continues growing&lt;/td&gt;
&lt;td&gt;Network latency or firewall&lt;/td&gt;
&lt;td&gt;Check network connectivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data isn't replicating&lt;/td&gt;
&lt;td&gt;Incorrect ReplicatedMergeTree path&lt;/td&gt;
&lt;td&gt;Verify ZooKeeper/Keeper paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost Keeper quorum&lt;/td&gt;
&lt;td&gt;Too many Keeper nodes offline&lt;/td&gt;
&lt;td&gt;Restore quorum by bringing nodes back online&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  Performance and Operational Best Practices
&lt;/h1&gt;

&lt;p&gt;To build a reliable replicated ClickHouse deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy an odd number of Keeper nodes (3 or 5) to maintain quorum.&lt;/li&gt;
&lt;li&gt;Assign every Keeper node a unique &lt;code&gt;server_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give every replica a unique name in &lt;code&gt;macros.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always execute DDL statements using &lt;code&gt;ON CLUSTER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Perform inserts through the Distributed table instead of local tables.&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;system.replicas&lt;/code&gt; regularly for replication lag.&lt;/li&gt;
&lt;li&gt;Restart cluster nodes one at a time.&lt;/li&gt;
&lt;li&gt;Ensure stable, low-latency connectivity between data centers.&lt;/li&gt;
&lt;li&gt;Batch inserts to reduce replication overhead.&lt;/li&gt;
&lt;li&gt;Continuously monitor Keeper health and replication queues.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  When Should You Use Cross-Datacenter Replication?
&lt;/h1&gt;

&lt;p&gt;Cross-datacenter replication is an excellent choice when your environment requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High availability&lt;/li&gt;
&lt;li&gt;Disaster recovery&lt;/li&gt;
&lt;li&gt;Multi-region deployments&lt;/li&gt;
&lt;li&gt;Read scalability&lt;/li&gt;
&lt;li&gt;Business continuity&lt;/li&gt;
&lt;li&gt;Low-latency regional analytics&lt;/li&gt;
&lt;li&gt;Fault-tolerant analytical platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Organizations operating globally often combine replicated tables with distributed queries to provide seamless analytics regardless of where users connect.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Cross-datacenter replication enables ClickHouse® to maintain synchronized copies of data across geographically distributed environments, improving availability, fault tolerance, and disaster recovery capabilities. By combining &lt;strong&gt;ReplicatedMergeTree&lt;/strong&gt;, &lt;strong&gt;ClickHouse Keeper&lt;/strong&gt;, and &lt;strong&gt;Distributed&lt;/strong&gt; tables, organizations can build resilient analytical platforms that continue serving queries even when individual nodes or data centers become unavailable.&lt;/p&gt;

&lt;p&gt;Although network latency and bandwidth should be considered when replicating across regions, following best practices such as batching inserts, maintaining a healthy Keeper cluster, monitoring replication queues, and using Distributed tables for application traffic helps ensure reliable and efficient synchronization.&lt;/p&gt;

&lt;p&gt;For organizations requiring both high-performance analytics and resilient multi-region deployments, ClickHouse provides a scalable and robust replication architecture that minimizes operational complexity while maximizing availability.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>devops</category>
      <category>database</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>What's the Difference Between RAG and Agent Memory?</title>
      <dc:creator>gregor</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:44:17 +0000</pubDate>
      <link>https://dev.to/plur9/whats-the-difference-between-rag-and-agent-memory-pil</link>
      <guid>https://dev.to/plur9/whats-the-difference-between-rag-and-agent-memory-pil</guid>
      <description>&lt;h1&gt;
  
  
  What's the Difference Between RAG and Agent Memory?
&lt;/h1&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) retrieves relevant passages from a fixed document collection at query time and pastes them into the prompt. Agent memory stores what the agent has &lt;em&gt;learned&lt;/em&gt; from interactions — corrections, preferences, decisions, behavioral patterns — and updates that knowledge over time. The distinction is not subtle: RAG is read-only retrieval from external corpora; agent memory is read-write learning with feedback loops, forgetting, and consolidation. RAG answers "find me a document about X"; agent memory answers "remember that we decided to use Vitest instead of Jest last week, and that you corrected me on the API endpoint last Tuesday."&lt;/p&gt;

&lt;h2&gt;
  
  
  The core difference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;Agent Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it stores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Documents, passages, chunks from external corpora&lt;/td&gt;
&lt;td&gt;What the agent learned from interactions — corrections, preferences, decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read / write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read-only at query time (the corpus is static)&lt;/td&gt;
&lt;td&gt;Read-write (the agent writes new memories, updates, and deletes old ones)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Corpus changes when you add/remove documents; no learned updates&lt;/td&gt;
&lt;td&gt;Memories are created, reinforced, decayed, and forgotten based on feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Documents persist; retrieval results do not accumulate&lt;/td&gt;
&lt;td&gt;Memories accumulate across sessions and improve over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No learning — same query retrieves the same passage&lt;/td&gt;
&lt;td&gt;Learns from corrections, feedback signals, and usage patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forgetting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No forgetting mechanism — all documents remain equally retrievable&lt;/td&gt;
&lt;td&gt;Supported by design — several systems apply time-based decay (ACT-R-inspired) so outdated memories lose retrieval strength&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source document is the provenance&lt;/td&gt;
&lt;td&gt;Each memory has provenance — who said it, when, in what context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vector embeddings in a vector store&lt;/td&gt;
&lt;td&gt;Varies: vector store, knowledge graph, YAML files, agent state blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction maps to a well-known one in cognitive science: &lt;strong&gt;semantic memory&lt;/strong&gt; (general knowledge, facts — what RAG provides) vs &lt;strong&gt;episodic and procedural memory&lt;/strong&gt; (what happened to you, what you learned to do — what agent memory provides). RAG gives the model access to a library. Agent memory gives it a notebook it writes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RAG is (and what it does well)
&lt;/h2&gt;

&lt;p&gt;RAG was formalized by Lewis et al. in 2020 ("Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks," NeurIPS 2020, &lt;a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer"&gt;arXiv:2005.11401&lt;/a&gt;). The architecture is simple: given a query, retrieve the top-k relevant passages from a document index, prepend them to the prompt, and let the model generate an answer conditioned on both the query and the retrieved context. A 2023 survey by Gao et al. (&lt;a href="https://arxiv.org/abs/2312.10997" rel="noopener noreferrer"&gt;arXiv:2312.10997&lt;/a&gt;) catalogued the explosion of RAG variants — pre-retrieval, post-retrieval, modular pipelines — and confirmed the core pattern: retrieve, augment, generate.&lt;/p&gt;

&lt;p&gt;RAG is excellent for &lt;strong&gt;question answering over known documents&lt;/strong&gt;. If you have a corpus of PDFs, a product manual, a codebase, or a wiki — RAG lets the model cite specific passages from that corpus. It is the standard approach for "chat with your documents" use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RAG cannot do
&lt;/h2&gt;

&lt;p&gt;RAG has four structural limitations for agent use cases:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No learning from interactions
&lt;/h3&gt;

&lt;p&gt;RAG retrieves from a static corpus. If you correct the model — "actually, we use Vitest, not Jest" — the retrieval pipeline does not update. Next session, the model retrieves the same documents and may make the same mistake. The corpus is fixed; the model's &lt;em&gt;behavioral&lt;/em&gt; knowledge does not accumulate. A 2024 survey of LLM-based agent memory mechanisms (Zhang et al., "A Survey on the Memory Mechanism of Large Language Model based Agents," &lt;a href="https://arxiv.org/abs/2404.13501" rel="noopener noreferrer"&gt;arXiv:2404.13501&lt;/a&gt;) identified this as the fundamental gap: "memory designs are scattered across different papers" because RAG addresses retrieval, not learning.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No preference persistence
&lt;/h3&gt;

&lt;p&gt;RAG cannot remember that you prefer tabs over spaces, that your database is PostgreSQL, or that you deprecated an API last week. These are not documents in a corpus — they are facts the agent &lt;em&gt;learned&lt;/em&gt; from interacting with you. RAG has nowhere to put them.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No forgetting or decay
&lt;/h3&gt;

&lt;p&gt;In RAG, all documents are equally retrievable forever. There is no mechanism to say "this fact is outdated" or "this preference changed." If you migrated from Jest to Vitest, both the old and new documentation sit in the index with equal standing. Agent memory systems can model forgetting explicitly — several projects adapt the ACT-R cognitive theory, applying time-based decay so outdated memories lose retrieval strength while recently-reinforced ones stay sharp (implementations vary: not every memory engine ships decay, but the architecture supports it, which RAG's does not).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. No feedback loops
&lt;/h3&gt;

&lt;p&gt;RAG retrieval is one-shot: query, retrieve, generate. There is no signal that says "that retrieval was helpful" or "that retrieval was irrelevant." Agent memory systems can close this loop: feedback signals (positive, negative, neutral) train the injection pipeline so recall quality improves with use. Not every engine implements feedback, but where it exists, the more you use it, the better it gets at surfacing the right memory at the right time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What agent memory adds beyond retrieval
&lt;/h2&gt;

&lt;p&gt;Agent memory systems perform four operations that RAG does not:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Capture&lt;/strong&gt; — Extract facts, corrections, preferences, and decisions from the conversation as they happen. Not a document dump; structured memory items (the agent learned you prefer tabs, that the database is PostgreSQL, that the auth flow uses JWT).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Store&lt;/strong&gt; — Persist those items outside the model in a format that survives session end. Formats vary: Mem0 uses a vector store, Letta uses agent state blocks, Zep uses a temporal knowledge graph, PLUR uses human-readable YAML files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieve (with context)&lt;/strong&gt; — At the start of the next session, surface the right memories for the current context. Not all of them — that would overflow the window. The relevant ones, selected by hybrid search (BM25 + embeddings), activation strength, and feedback signals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update and forget&lt;/strong&gt; — When a fact changes, update the memory. When a fact is wrong, correct it. When you want something deleted, delete it. Memories decay over time if not reinforced, so the system does not accumulate stale noise.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not theoretical. MemGPT (Packer et al., 2023, "MemGPT: Towards LLMs as Operating Systems," &lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;arXiv:2310.08560&lt;/a&gt;) demonstrated that treating memory like an OS manages memory tiers — core memory in the context window, archival memory retrieved on demand — dramatically improves agent performance on multi-session tasks. The key insight: agents need &lt;em&gt;managed&lt;/em&gt; memory, not just &lt;em&gt;more&lt;/em&gt; context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you use both?
&lt;/h2&gt;

&lt;p&gt;Yes, and many agent architectures do. RAG and agent memory serve different layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt; handles the &lt;strong&gt;knowledge layer&lt;/strong&gt; — what documents, code, and reference material the agent can access. You point it at your codebase, your docs, your wiki.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent memory&lt;/strong&gt; handles the &lt;strong&gt;experience layer&lt;/strong&gt; — what the agent has learned from working with you, what preferences it has accumulated, what mistakes it has been corrected on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A coding agent might use RAG to search your repository for a function definition, and use agent memory to remember that you prefer functional style over OOP. The two systems are complementary, not competitive. The mistake is thinking RAG alone gives your agent memory — it gives your agent a library, but no notebook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape: who does what
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;RAG?&lt;/th&gt;
&lt;th&gt;Agent Memory?&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LangChain RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (core feature)&lt;/td&gt;
&lt;td&gt;Partial (memory modules, but basic)&lt;/td&gt;
&lt;td&gt;Various module types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LlamaIndex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (core feature)&lt;/td&gt;
&lt;td&gt;Partial (chat history buffers)&lt;/td&gt;
&lt;td&gt;Buffer, summary, vector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mem0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (memory layer)&lt;/td&gt;
&lt;td&gt;Vector store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Letta&lt;/strong&gt; (formerly MemGPT)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (stateful agent OS)&lt;/td&gt;
&lt;td&gt;Agent state blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zep / Graphiti&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (temporal knowledge graph)&lt;/td&gt;
&lt;td&gt;Knowledge graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cognee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes (graph + vector + relational)&lt;/td&gt;
&lt;td&gt;Own data model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PLUR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (engram engine + MCP)&lt;/td&gt;
&lt;td&gt;Human-readable YAML (open format)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key differentiator within agent memory is &lt;strong&gt;format openness&lt;/strong&gt;. Mem0 and Letta are open-source (Apache-2.0) but store memories in opaque formats — vector embeddings or agent state blocks you cannot read in a text editor. PLUR (Apache-2.0, &lt;a href="https://github.com/plur-ai/plur" rel="noopener noreferrer"&gt;github.com/plur-ai/plur&lt;/a&gt;) stores memories as YAML files you can open, edit, diff, and version-control. The MCP (Model Context Protocol, &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25" rel="noopener noreferrer"&gt;specification 2025-11-25&lt;/a&gt;) makes the transport layer open; the memory format is the differentiator.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you need…&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search and cite a fixed document corpus&lt;/td&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer questions over your codebase, docs, or wiki&lt;/td&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember user preferences across sessions&lt;/td&gt;
&lt;td&gt;Agent memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learn from corrections and not repeat mistakes&lt;/td&gt;
&lt;td&gt;Agent memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Track how facts change over time&lt;/td&gt;
&lt;td&gt;Agent memory (Zep for temporal, PLUR for decay-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect, edit, and correct what the agent knows&lt;/td&gt;
&lt;td&gt;Agent memory with open format (PLUR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both (search documents AND remember interactions)&lt;/td&gt;
&lt;td&gt;RAG + agent memory (complementary)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is agent memory just RAG with extra steps?&lt;/strong&gt; No. RAG retrieves from a static document collection at query time. Agent memory stores what the agent learned from interactions, updates it with feedback, and decays outdated entries. RAG is a read-only retrieval pipeline; agent memory is a read-write learning system with forgetting and consolidation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can RAG replace agent memory?&lt;/strong&gt; Only if your agent never needs to remember preferences, corrections, or decisions across sessions. If your agent works in a single session or only queries fixed documents, RAG is sufficient. If your agent accumulates knowledge over time — learning your coding style, remembering architectural decisions, not repeating corrected mistakes — you need agent memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you use RAG and agent memory together?&lt;/strong&gt; Yes. They serve different layers: RAG handles the knowledge layer (documents, code, reference material), agent memory handles the experience layer (what the agent learned from working with you). Many agent architectures combine both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is agent memory slower than RAG?&lt;/strong&gt; Not necessarily. Both involve a retrieval step at query time. Agent memory adds a write step (capturing memories during the conversation) and a decay/feedback step (updating memory strength), but these are background operations that do not add latency to the inference path. Zep reports sub-200ms retrieval for its temporal knowledge graph (docs.getzep.com).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the ACT-R decay model in agent memory?&lt;/strong&gt; ACT-R is a cognitive architecture from cognitive science that models how human memory decays over time. Several agent memory systems adapt it: memories that are not accessed lose retrieval strength, while memories that are reinforced (accessed or given positive feedback) gain strength. This prevents the memory store from accumulating stale noise — something RAG cannot do because all documents remain equally retrievable.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>opensource</category>
      <category>memory</category>
      <category>agents</category>
    </item>
    <item>
      <title>TuneWatch: A Private Space for the Entertainment We Love Together</title>
      <dc:creator>Thierno Abdoulaye Diallo</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:37:52 +0000</pubDate>
      <link>https://dev.to/taddiallo/tunewatch-a-private-space-for-the-entertainment-we-love-together-16od</link>
      <guid>https://dev.to/taddiallo/tunewatch-a-private-space-for-the-entertainment-we-love-together-16od</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the *&lt;/em&gt;&lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;*&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;TuneWatch&lt;/strong&gt;, a private collaborative media tracker for small groups.&lt;/p&gt;

&lt;p&gt;TuneWatch gives friends, couples, families, and small communities a shared place to keep track of the entertainment they care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;movies&lt;/li&gt;
&lt;li&gt;anime&lt;/li&gt;
&lt;li&gt;books&lt;/li&gt;
&lt;li&gt;music&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can create an account, choose a profile avatar, use a personal tracker, create private groups, invite other people with a code, add recommendations, rate entries, search their shared library, and view statistics based on real activity.&lt;/p&gt;

&lt;p&gt;The idea is simple: the things we love are rarely enjoyed alone.&lt;/p&gt;

&lt;p&gt;When I discover a great movie, finish an anime, read a memorable book, or hear a song I cannot stop replaying, I usually want to share it with someone. TuneWatch turns those scattered recommendations into one private and organized space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Self-service account creation&lt;/li&gt;
&lt;li&gt;Username and avatar setup&lt;/li&gt;
&lt;li&gt;Personal tracking space&lt;/li&gt;
&lt;li&gt;Private groups of up to five members&lt;/li&gt;
&lt;li&gt;Invite codes&lt;/li&gt;
&lt;li&gt;Real-time shared entries&lt;/li&gt;
&lt;li&gt;Movies, anime, books, and music&lt;/li&gt;
&lt;li&gt;Ratings, statuses, notes, and tags&lt;/li&gt;
&lt;li&gt;Search&lt;/li&gt;
&lt;li&gt;Group statistics&lt;/li&gt;
&lt;li&gt;Mobile-first interface&lt;/li&gt;
&lt;li&gt;Installable web application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interface was designed around mobile use and inspired by the clarity, spacing, rounded surfaces, and comfortable touch targets found in Samsung One UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Live application
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tunewatch.web.app" rel="noopener noreferrer"&gt;Open TuneWatch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A new visitor can create an account directly in the application, choose an avatar and username, and start using a personal tracker without requiring an administrator.&lt;/p&gt;

&lt;p&gt;If you'd like to explore TuneWatch without creating a new account, you can use the demo account below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;demo@tunewatch.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Video demonstration
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://youtu.be/R8BPn2_3QXs" rel="noopener noreferrer"&gt;Watch the TuneWatch demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demonstration covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating an account&lt;/li&gt;
&lt;li&gt;Configuring a profile&lt;/li&gt;
&lt;li&gt;Adding an entry&lt;/li&gt;
&lt;li&gt;Searching the tracker&lt;/li&gt;
&lt;li&gt;Creating and joining groups&lt;/li&gt;
&lt;li&gt;Viewing statistics&lt;/li&gt;
&lt;li&gt;Editing the profile&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The source code is available on GitHub:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;TuneWatch uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;Firebase Authentication&lt;/li&gt;
&lt;li&gt;Cloud Firestore&lt;/li&gt;
&lt;li&gt;Firebase Hosting&lt;/li&gt;
&lt;li&gt;Lucide icons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firebase Authentication handles account creation and login.&lt;/p&gt;

&lt;p&gt;Cloud Firestore stores user profiles, groups, memberships, invite codes, and media entries. Each group owns its own entries subcollection, and Firestore listeners keep the tracker synchronized in real time.&lt;/p&gt;

&lt;p&gt;The application is organized into separate layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pages
  ↓
Components
  ↓
Hooks
  ↓
Services
  ↓
Firebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hooks manage authentication, groups, and entries. Firebase operations remain inside dedicated service files, while the pages focus on the user interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Personal and shared tracking
&lt;/h3&gt;

&lt;p&gt;Every new user receives a personal space so that TuneWatch remains useful even when used alone.&lt;/p&gt;

&lt;p&gt;A user can later create or join additional groups. Selecting, creating, or joining a group immediately activates it and returns the user to the Tracker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real data instead of mock content
&lt;/h3&gt;

&lt;p&gt;The Tracker, Search, Groups, Statistics, and Profile screens all use real Firebase data.&lt;/p&gt;

&lt;p&gt;Statistics such as completed entries, planned entries, average rating, and favorite category are derived dynamically from the active group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile-first design
&lt;/h3&gt;

&lt;p&gt;I wanted TuneWatch to feel like a mobile product rather than a desktop dashboard squeezed onto a phone.&lt;/p&gt;

&lt;p&gt;That led to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a persistent bottom navigation bar&lt;/li&gt;
&lt;li&gt;large touch targets&lt;/li&gt;
&lt;li&gt;rounded cards&lt;/li&gt;
&lt;li&gt;bottom sheets for important actions&lt;/li&gt;
&lt;li&gt;short and natural interface text&lt;/li&gt;
&lt;li&gt;graceful empty states&lt;/li&gt;
&lt;li&gt;a consistent visual hierarchy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Hardest Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Secure group membership
&lt;/h3&gt;

&lt;p&gt;Joining and leaving groups required more than updating an array.&lt;/p&gt;

&lt;p&gt;Group membership is limited to five people, so joins use Firestore transactions to avoid race conditions. Security rules also ensure that users cannot arbitrarily modify another group's members or entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping the active group consistent
&lt;/h3&gt;

&lt;p&gt;TuneWatch supports both personal and shared spaces.&lt;/p&gt;

&lt;p&gt;The application saves the active group in the user's Firestore profile and restores it after login. This keeps the experience continuous across sessions and devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning a web app into a mobile experience
&lt;/h3&gt;

&lt;p&gt;The application was tested directly on a phone throughout development.&lt;/p&gt;

&lt;p&gt;This exposed issues that were easy to miss on desktop, including touch target sizes, content hidden behind the navigation bar, inconsistent dialogs, caching, and the installed application's name and icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection to the Theme
&lt;/h2&gt;

&lt;p&gt;The challenge theme is &lt;strong&gt;Passion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For me, passion is not only the thing we love. It is also the need to talk about it, recommend it, remember it, and experience it with other people.&lt;/p&gt;

&lt;p&gt;TuneWatch was built around that shared side of passion.&lt;/p&gt;

&lt;p&gt;It is not a large public social network. It is a smaller and more personal space for the people whose recommendations actually matter to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Work
&lt;/h2&gt;

&lt;p&gt;TuneWatch began as an earlier experimental tracker.&lt;/p&gt;

&lt;p&gt;During the challenge period, I substantially rebuilt and completed the product experience, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the mobile navigation and five-screen structure&lt;/li&gt;
&lt;li&gt;account creation and profile onboarding&lt;/li&gt;
&lt;li&gt;selectable avatars&lt;/li&gt;
&lt;li&gt;personal tracking spaces&lt;/li&gt;
&lt;li&gt;group flows&lt;/li&gt;
&lt;li&gt;dynamic statistics&lt;/li&gt;
&lt;li&gt;Firebase Hosting deployment&lt;/li&gt;
&lt;li&gt;the mobile-first visual system&lt;/li&gt;
&lt;li&gt;installable application branding&lt;/li&gt;
&lt;li&gt;the final demo and documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am including this note to be transparent about the project's development history.&lt;/p&gt;

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

&lt;p&gt;This project reinforced that a product is not finished when the main features technically work.&lt;/p&gt;

&lt;p&gt;Small details had a major effect on the final experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where the user lands after joining a group&lt;/li&gt;
&lt;li&gt;whether an empty Tracker feels welcoming&lt;/li&gt;
&lt;li&gt;whether a dialog belongs to the same visual system&lt;/li&gt;
&lt;li&gt;whether a username appears instead of a raw email&lt;/li&gt;
&lt;li&gt;whether an installed application still says “React App”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building TuneWatch required thinking about the full journey rather than isolated components.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;After the challenge, I would like to explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;richer media metadata and cover artwork&lt;/li&gt;
&lt;li&gt;more media categories&lt;/li&gt;
&lt;li&gt;notifications&lt;/li&gt;
&lt;li&gt;offline support&lt;/li&gt;
&lt;li&gt;native Android and iOS versions&lt;/li&gt;
&lt;li&gt;better group activity insights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would you track with your own TuneWatch group?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
  </channel>
</rss>
