<?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: Akshay Nikhare</title>
    <description>The latest articles on DEV Community by Akshay Nikhare (@akshay_nikhare).</description>
    <link>https://dev.to/akshay_nikhare</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044940%2F6ce7a144-ccd8-4fe8-9b74-955373350dbb.gif</url>
      <title>DEV Community: Akshay Nikhare</title>
      <link>https://dev.to/akshay_nikhare</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshay_nikhare"/>
    <language>en</language>
    <item>
      <title>IMOS IX : hardware Häfele Espagnolette Lock</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 08:04:14 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/imos-ix-hardware-hafele-espagnolette-lock-2522</link>
      <guid>https://dev.to/akshay_nikhare/imos-ix-hardware-hafele-espagnolette-lock-2522</guid>
      <description>&lt;p&gt;Adding branded hardware to a furniture model is where parametric design gets real. In this walkthrough we take a Häfele &lt;strong&gt;&lt;a href="https://www.hafeleindia.com/en/product/espagnolette-lock-nickel/P-01503387/" rel="noopener noreferrer"&gt;Espagnolette lock (nickel, article P-01503387)&lt;/a&gt;&lt;/strong&gt; and set it up in IMOS IX as a rotating-door lock connector — configuring the constructive connector characteristics, understanding the part numbering, and generating the export file that drives the CNC.&lt;/p&gt;

&lt;p&gt;The result: drop the lock into any cabinet or wardrobe door and IMOS IX automatically places the cylinder, the rod guides, and every strike position the workshop needs.&lt;/p&gt;

&lt;p&gt;Download the ready-made connector&lt;/p&gt;

&lt;p&gt;Import &lt;code&gt;Lock_ESP_v1.zip&lt;/code&gt; straight into IMOS IX and start configuring — no setup required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cadnative.com/downloads/Lock_ESP_v1.zip" rel="noopener noreferrer"&gt;Download Lock_ESP_v1.zip&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an Espagnolette lock?
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;Espagnolette lock&lt;/strong&gt; is a multi-point locking mechanism. A single cylinder drives a vertical rod (the espagnolette) up and down, engaging the carcase at the top and bottom of a tall door. Unlike a simple cam lock, it secures the door along its full height, which is why it is common on wardrobes, filing towers, and workshop cabinets.&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%2F8noyg63co73fnc88svio.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%2F8noyg63co73fnc88svio.png" alt="Häfele Espagnolette lock, nickel finish, article P-01503387, with removable-core cylinder and keys" width="800" height="832"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The physical fitting: Häfele Espagnolette lock, nickel (P-01503387), with removable-core cylinder and keys.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Häfele unit we are modelling is the nickel-finish variant, article &lt;strong&gt;&lt;a href="https://www.hafeleindia.com/en/product/espagnolette-lock-nickel/P-01503387/" rel="noopener noreferrer"&gt;P-01503387&lt;/a&gt;&lt;/strong&gt;, driven by a removable-core Symo cylinder. From a manufacturing point of view the lock is not one part but a small assembly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lock case with the cylinder, mortised into the door.&lt;/li&gt;
&lt;li&gt;The espagnolette rod running the height of the door.&lt;/li&gt;
&lt;li&gt;Rod guides holding the rod in line.&lt;/li&gt;
&lt;li&gt;Lock plates / strike receivers on the top and bottom shelves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those elements implies holes and pockets that have to be cut accurately — the perfect job for IMOS IX.&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%2F09tp92kofun66rt1xze7.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%2F09tp92kofun66rt1xze7.png" alt="IMOS IX drawing of a double door with the Espagnolette lock connector installed — central cylinder, rod running the full height, hooks and guides top and bottom" width="637" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same lock modelled in IMOS IX: the connector places the cylinder, the full-height rod, and the hooks and guides automatically.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing the connector into IMOS IX
&lt;/h2&gt;

&lt;p&gt;If you just want to use the lock, grab the &lt;a href="https://cadnative.com/downloads/Lock_ESP_v1.zip" rel="noopener noreferrer"&gt;&lt;code&gt;Lock_ESP_v1.zip&lt;/code&gt;&lt;/a&gt; download above — it contains the pre-built connector, ready to import through the &lt;strong&gt;iX Organizer&lt;/strong&gt;. You do not need to model anything from scratch.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; &lt;code&gt;Lock_ESP_v1.zip&lt;/code&gt; to a temporary folder.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;iX Organizer&lt;/strong&gt; and go to &lt;strong&gt;Integration Center › Import&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Open Import Medium&lt;/strong&gt; and select the unzipped import file.&lt;/li&gt;
&lt;li&gt;In the tree, expand &lt;strong&gt;Import medium › Hardware and Machining › Connector&lt;/strong&gt;. You will see the Espagnolette lock elements:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HAF_LOCK_ESP_Case_22464618&lt;/code&gt; — the lock case&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HAF_LOCK_ESP_Guide&lt;/code&gt; — rod guide&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HAF_LOCK_ESP_Hook_Top&lt;/code&gt; / &lt;code&gt;HAF_LOCK_ESP_Hook_Bot&lt;/code&gt; — top and bottom hooks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HAF_LOCK_ESP_LockingBolt&lt;/code&gt; — the locking bolt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HAF_LOCK_ESP_Rod&lt;/code&gt; — the espagnolette rod&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tick the connectors&lt;/strong&gt; you want (or the whole Connector folder) and click &lt;strong&gt;Import&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fa6du4f0ggzy5puscfklg.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%2Fa6du4f0ggzy5puscfklg.png" alt="iX Organizer Integration Center Import screen showing the HAF_LOCK_ESP connector elements ready to import" width="799" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;iX Organizer › Integration Center › Import — the HAF_LOCK_ESP connector elements from &lt;code&gt;Lock_ESP_v1.zip&lt;/code&gt;, ready to import.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Items shown in &lt;strong&gt;green&lt;/strong&gt; are new to your library; an item in &lt;strong&gt;orange&lt;/strong&gt; (like the locking bolt above) already exists and will be updated. After importing, open the connector once and check the editable characteristics (backset, bolt depth, rod offsets) against your own hardware.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once imported, the Espagnolette lock appears in your connector list under &lt;strong&gt;Element Manager › Connections › Connector&lt;/strong&gt;, ready to attach to a rotating-door article. The rest of this article explains how the connector is built — useful if you want to tweak it, rebuild it for another fitting, or understand what the import gives you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constructive connector characteristics
&lt;/h2&gt;

&lt;p&gt;In IMOS IX a lock is a &lt;strong&gt;connector&lt;/strong&gt;, and its behaviour is driven by the &lt;em&gt;constructive connector characteristics&lt;/em&gt;. You reach them here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ribbon: Main data › Main data › Element Manager › Connections › Connector&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The constructive connector characteristics describe features of the connector — for example, where it sits in the article and how its machining is derived. For rotating doors, IMOS IX distinguishes between &lt;strong&gt;Lock&lt;/strong&gt;, &lt;strong&gt;Hinges&lt;/strong&gt;, &lt;strong&gt;Mounting plates&lt;/strong&gt;, and &lt;strong&gt;Buffers and Openers&lt;/strong&gt;. For the Espagnolette lock we are concerned with the &lt;strong&gt;Lock&lt;/strong&gt; characteristics below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Element characteristics (LS_TYP)
&lt;/h3&gt;

&lt;p&gt;The connector type is defined in the &lt;code&gt;LS_TYP&lt;/code&gt; (Locking system type) characteristic. These sub-elements build up the lock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LFRONT&lt;/code&gt;&lt;/strong&gt; — Lock cylinder, key plug, turning handle (the front machining)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LPLATE&lt;/code&gt;&lt;/strong&gt; — Lock plate, stop bracket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LRODHOLDER&lt;/code&gt;&lt;/strong&gt; — Rod guide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LPIN&lt;/code&gt;&lt;/strong&gt; — Positioning pin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LOCK&lt;/code&gt;&lt;/strong&gt; — Lock case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LACT&lt;/code&gt; / &lt;code&gt;LACT_L&lt;/code&gt; / &lt;code&gt;LACT_R&lt;/code&gt;&lt;/strong&gt; — Door latch (both / left / right)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ROD&lt;/code&gt;&lt;/strong&gt; — Espagnolette rod&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Order quantity of rod guides controls how many guides/mountings are ordered for the espagnolette rods, written to the &lt;code&gt;IDBPURCH&lt;/code&gt; table. Use &lt;strong&gt;Order quantity only&lt;/strong&gt; to set it manually, or &lt;strong&gt;Real quantity times order quantity&lt;/strong&gt; to derive it from the linear division. Note: the &lt;em&gt;Division rod&lt;/em&gt; characteristic does &lt;strong&gt;not&lt;/strong&gt; write the real amount into &lt;code&gt;IDBPURCH&lt;/code&gt; — use &lt;em&gt;Order quantity of rod guides&lt;/em&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Editable dimensional characteristics
&lt;/h3&gt;

&lt;p&gt;These are the numbers you tune to match the Häfele fitting drawing for P-01503387:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_X&lt;/code&gt; — Locking latch reveal:&lt;/strong&gt; how far the lock latch is revealed in the carcass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_LOCK_ROD_SUB&lt;/code&gt; — Espagnolette reduction middle:&lt;/strong&gt; how the rod is shortened at the centre (lock section); omit for locks without rods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_LACT_ROD_SUB&lt;/code&gt; — Espagnolette reduction door latch:&lt;/strong&gt; shortening of the rod at the lock latch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_BOLD_DEP&lt;/code&gt; — Bolt depth:&lt;/strong&gt; the bolt depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_BOLD_SIZE&lt;/code&gt; — Backset:&lt;/strong&gt; distance between the insertion point of the lock (usually its middle) and the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_BOLD_DIST&lt;/code&gt; — Distance bolt/door back:&lt;/strong&gt; door back-panel distance to the bolt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_ROD_DIST&lt;/code&gt; — Offset espagnolette back:&lt;/strong&gt; how far the espagnolette is shifted into the carcass (always set this; omit only for locks without rods).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_PLATE_DEP&lt;/code&gt; — Depth lock plate:&lt;/strong&gt; maximum depth the bolt travels into the lock plate (optional).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_PLATE_SIZE&lt;/code&gt; — Overlap lock plate:&lt;/strong&gt; how far the lock plate protrudes from the carcass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_PIN_DIST&lt;/code&gt; — Distance door/pin:&lt;/strong&gt; door back-panel distance to the positioning-pin centre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LS_ROD_CENTER_DIST&lt;/code&gt; — Offset to hinge side:&lt;/strong&gt; moves the espagnolette (and its hooks) toward the hinge side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rod positioning and geometry
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Division rod (&lt;code&gt;LS_LINDIV&lt;/code&gt;)&lt;/strong&gt; — a Linear Division descriptor that positions the espagnolette along the door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Espagnolette side (&lt;code&gt;LS_ROD_SIDES&lt;/code&gt;)&lt;/strong&gt; — where the rod runs relative to the lock pocket: &lt;code&gt;0&lt;/code&gt; both sides, &lt;code&gt;1&lt;/code&gt; just top, &lt;code&gt;2&lt;/code&gt; just bottom, &lt;code&gt;3&lt;/code&gt; only left, &lt;code&gt;4&lt;/code&gt; only right, &lt;code&gt;5&lt;/code&gt; through the lock pocket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locksystem DWG (&lt;code&gt;SECTION_DWG&lt;/code&gt;)&lt;/strong&gt; — a DWG holding the rod's cross-section geometry. It may contain only &lt;strong&gt;one polyline&lt;/strong&gt;, and the file must live in the &lt;code&gt;&amp;lt;Library&amp;gt;\ConnDWG&lt;/code&gt; directory (the &lt;code&gt;.dwg&lt;/code&gt; extension is optional in the field).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part numbers for rotating doors
&lt;/h2&gt;

&lt;p&gt;The part numbers of the Espagnolette lock are relevant to the constructive connector characteristics — they tell IMOS IX (and you) which panel each piece of machining belongs to. The numbering mirrors depending on whether the door is hinged left or right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Diagram omitted in this syndicated copy — see the &lt;a href="https://cadnative.com/blog/imos-ix-espagnolette-lock-export" rel="noopener noreferrer"&gt;original article&lt;/a&gt; for the illustration.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Left door&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Diagram omitted in this syndicated copy — see the &lt;a href="https://cadnative.com/blog/imos-ix-espagnolette-lock-export" rel="noopener noreferrer"&gt;original article&lt;/a&gt; for the illustration.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Right door&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Left door&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; — Front machining&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; — Top shelf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; — Bottom shelf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6&lt;/strong&gt; — Front machining top&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7&lt;/strong&gt; — Front machining bottom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right door&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; — Front machining&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; — Bottom shelf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; — Top shelf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6&lt;/strong&gt; — Front machining bottom&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7&lt;/strong&gt; — Front machining top&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the mirror: on the left door part &lt;code&gt;2&lt;/code&gt; is the top shelf, while on the right door part &lt;code&gt;2&lt;/code&gt; is the bottom shelf. Getting the handing right is what keeps the strike machining on the correct panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the export file
&lt;/h2&gt;

&lt;p&gt;Once a variant is configured, IMOS IX evaluates the article and produces the production data. The &lt;strong&gt;export file&lt;/strong&gt; typically contains three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Machine data&lt;/strong&gt; — DXF or MPR/MPR2 files carrying every bore and pocket for the CNC router, including the cylinder, rod guides, and strikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part list / BOM&lt;/strong&gt; — the panel parts plus the Häfele Espagnolette lock (P-01503387) as a purchased fitting, with rod-guide quantities from &lt;code&gt;IDBPURCH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label and assembly data&lt;/strong&gt; — so cut parts arrive pre-marked for the fitter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A typical export run 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;Configured door + lock → IMOS IX article → CNC files (DXF/MPR) + BOM → Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the generated MPR in your machine's viewer and you should see the cylinder bore on the front, the rod guides down the door, and the strike positions on the top and bottom shelves — exactly where the connector characteristics placed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the output
&lt;/h2&gt;

&lt;p&gt;Before sending anything to the machine, sanity-check the export:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backset (&lt;code&gt;LS_BOLD_SIZE&lt;/code&gt;) and bolt depth (&lt;code&gt;LS_BOLD_DEP&lt;/code&gt;) match the Häfele drawing for P-01503387.&lt;/li&gt;
&lt;li&gt;Rod guides (&lt;code&gt;LRODHOLDER&lt;/code&gt;) line up along the espagnolette and the quantity in the BOM is correct.&lt;/li&gt;
&lt;li&gt;Strike machining lands on the right panel for the door's handing (check the part numbers above).&lt;/li&gt;
&lt;li&gt;The BOM lists the correct article number and nickel finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Catching a wrong backset in the viewer costs seconds; catching it after a batch of doors is cut costs a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talk to us
&lt;/h2&gt;

&lt;p&gt;We build IMOS IX hardware libraries and connector setups for furniture manufacturers across Europe. If you need help configuring Häfele fittings or automating your CNC export, &lt;a href="https://www.cadnative.com/#contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cadnative.com/blog/imos-ix-espagnolette-lock-export" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>imosix</category>
      <category>hfele</category>
      <category>hardware</category>
      <category>cncexport</category>
    </item>
    <item>
      <title>FireVision TV: Native IPTV Streaming for Fire TV</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 07:49:07 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/firevision-tv-native-iptv-streaming-for-fire-tv-1fc8</link>
      <guid>https://dev.to/akshay_nikhare/firevision-tv-native-iptv-streaming-for-fire-tv-1fc8</guid>
      <description>&lt;p&gt;FireVision TV is an IPTV streaming solution designed for Fire TV devices and Android-based set-top boxes. It delivers live channels, on-demand content, and catch-up TV through a clean, native interface — without the sluggishness that plagues most generic IPTV players.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;Live portal&lt;/a&gt; · &lt;a href="https://github.com/akshaynikhare/FireVisionIPTV" rel="noopener noreferrer"&gt;Client app&lt;/a&gt; · &lt;a href="https://github.com/akshaynikhare/FireVisionIPTVServer" rel="noopener noreferrer"&gt;Server&lt;/a&gt; · &lt;a href="https://github.com/akshaynikhare/FireVisionIPTV/releases/" rel="noopener noreferrer"&gt;Releases / APK&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is FireVision TV?
&lt;/h2&gt;

&lt;p&gt;FireVision TV is a purpose-built IPTV client developed by Cadnative. Unlike web-based or cross-compiled players, it's written natively for Fire OS — which means smoother playback, faster channel switching, and proper integration with Alexa voice control.&lt;/p&gt;

&lt;p&gt;Key capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live TV&lt;/strong&gt; — thousands of channels across sports, news, entertainment, and regional content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VOD library&lt;/strong&gt; — movies and series available on-demand with resumable playback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-up TV&lt;/strong&gt; — watch programmes you missed, up to 7 days back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EPG (Electronic Programme Guide)&lt;/strong&gt; — full 7-day grid with programme details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-screen&lt;/strong&gt; — stream on up to 4 devices simultaneously on a single subscription&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why native matters for Fire TV
&lt;/h2&gt;

&lt;p&gt;Most IPTV apps ship a single React Native or Flutter bundle and call it done. The result is frame drops during fast channel zaps, input lag on the d-pad, and memory pressure that causes the Fire TV to throttle after 20 minutes.&lt;/p&gt;

&lt;p&gt;FireVision TV is built in Kotlin against the Fire TV SDK directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ExoPlayer&lt;/strong&gt; handles HLS/DASH/RTMP with hardware-accelerated decoding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leanback&lt;/strong&gt; library powers the d-pad-first UI navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alexa integration&lt;/strong&gt; lets users say "tune to Sky Sports" or "play Succession" without touching the remote&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: channel changes in under 1 second, and stable 4K HDR playback on Fire TV Stick 4K Max.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up FireVision TV
&lt;/h2&gt;

&lt;p&gt;Getting started takes under two minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; — download the latest APK from the &lt;a href="https://github.com/akshaynikhare/FireVisionIPTV/releases/" rel="noopener noreferrer"&gt;FireVision TV releases page&lt;/a&gt; and sideload it via ADB, or search "FireVision TV" on the Amazon Appstore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activate&lt;/strong&gt; — open the app, note your device code, and enter it at &lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;tv.cadnative.com&lt;/a&gt; with your subscription credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load your playlist&lt;/strong&gt; — paste your M3U URL or Xtream Codes API details in Settings → Playlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done&lt;/strong&gt; — channels populate automatically. The EPG syncs in the background.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sideload via ADB (developer mode must be enabled on the Fire TV)&lt;/span&gt;
adb connect &amp;lt;fire-tv-ip&amp;gt;:5555
adb &lt;span class="nb"&gt;install &lt;/span&gt;firevision-tv.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  EPG and channel management
&lt;/h2&gt;

&lt;p&gt;The built-in EPG pulls data from XMLTV sources. You can configure your own source or use the default one bundled with your subscription:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Settings → EPG Source&lt;/strong&gt; → paste your XMLTV URL&lt;/li&gt;
&lt;li&gt;EPG refreshes every 12 hours automatically&lt;/li&gt;
&lt;li&gt;Map channels to EPG entries manually if the auto-match doesn't align&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The channel list supports grouping by category (Sports, Movies, News), and you can create a &lt;strong&gt;Favourites&lt;/strong&gt; row that appears at the top of the home screen for one-click access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parental controls
&lt;/h2&gt;

&lt;p&gt;FireVision TV supports PIN-based content locking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lock individual channels or entire categories&lt;/li&gt;
&lt;li&gt;Set a PIN in &lt;strong&gt;Settings → Parental Controls&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The PIN is required before any locked content plays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is stored locally on the device — no account-level parental data is sent to our servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting common issues
&lt;/h2&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;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buffering on live TV&lt;/td&gt;
&lt;td&gt;Switch stream quality to Auto or reduce to 720p in Settings → Playback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPG not loading&lt;/td&gt;
&lt;td&gt;Check your XMLTV URL is accessible; try a manual refresh in Settings → EPG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black screen on channel switch&lt;/td&gt;
&lt;td&gt;Force-stop the app and clear cache (Fire TV Settings → Manage Installed Apps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App not installing&lt;/td&gt;
&lt;td&gt;Enable Apps from Unknown Sources in Fire TV Developer Options for sideloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Subscription and pricing
&lt;/h2&gt;

&lt;p&gt;FireVision TV uses a subscription model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly&lt;/strong&gt; — flexible, cancel anytime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6-month&lt;/strong&gt; — 15% saving vs monthly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annual&lt;/strong&gt; — best value, 30% saving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All plans include full VOD access, EPG, and multi-screen support. A 24-hour free trial is available — no credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;p&gt;The roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromecast support&lt;/strong&gt; — cast from your phone to any TV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download for offline&lt;/strong&gt; — save VOD content for playback without internet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sports notifications&lt;/strong&gt; — get alerts before a match kicks off&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire TV Cube&lt;/strong&gt; — hands-free voice control optimisations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open source &amp;amp; resources
&lt;/h2&gt;

&lt;p&gt;FireVision TV is open source. Explore the code, report issues, or contribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/akshaynikhare/FireVisionIPTV" rel="noopener noreferrer"&gt;FireVisionIPTV&lt;/a&gt; — the Fire TV / Android client app&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/akshaynikhare/FireVisionIPTVServer" rel="noopener noreferrer"&gt;FireVisionIPTVServer&lt;/a&gt; — the backend server powering activation, EPG, and subscriptions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/akshaynikhare/FireVisionIPTV/releases/" rel="noopener noreferrer"&gt;Releases &amp;amp; APK downloads&lt;/a&gt; — grab the latest build&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;Live portal&lt;/a&gt; — device activation and account management&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're building a custom IPTV solution or need a white-label Fire TV app for your streaming service, &lt;a href="https://www.cadnative.com/#contact" rel="noopener noreferrer"&gt;get in touch with us at Cadnative&lt;/a&gt; — FireVision TV is available as a white-label platform with full branding customisation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cadnative.com/blog/firevision-tv-iptv-streaming" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firevisiontv</category>
      <category>iptv</category>
      <category>firetv</category>
      <category>streaming</category>
    </item>
    <item>
      <title>Introducing GateKeeper Zero: Society Management, Reimagined</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 07:48:53 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/introducing-gatekeeper-zero-society-management-reimagined-22n0</link>
      <guid>https://dev.to/akshay_nikhare/introducing-gatekeeper-zero-society-management-reimagined-22n0</guid>
      <description>&lt;p&gt;GateKeeper Zero is a full-stack society management platform built for gated communities, residential complexes, and housing societies. It connects residents, security staff, and society administrators through a unified system — replacing paper registers, WhatsApp groups, and fragmented tooling with a single source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gatekeeperzero.com/" rel="noopener noreferrer"&gt;→ Visit gatekeeperzero.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Managing a gated community involves more actors than it appears: residents, visitors, delivery agents, domestic staff, security guards, and committee members — all interacting daily, mostly without a reliable audit trail.&lt;/p&gt;

&lt;p&gt;Common pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security guards manually logging visitor entries in paper registers&lt;/li&gt;
&lt;li&gt;No way for residents to pre-approve guests or track delivery status&lt;/li&gt;
&lt;li&gt;Committee members sharing decisions via WhatsApp with no accountability&lt;/li&gt;
&lt;li&gt;No digital record of maintenance complaints or their resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GateKeeper Zero addresses all of these through role-specific mobile apps and a backend API designed around these workflows.&lt;/p&gt;

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

&lt;p&gt;The platform has three main layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ZeroApi&lt;/strong&gt; — a Node.js REST API (Express + PostgreSQL) that drives all business logic, role-based access control, and push notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zeroMobileResident&lt;/strong&gt; — a React Native app for residents (iOS + Android)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zeroMobileSociety&lt;/strong&gt; — a React Native app for security guards and society admins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each app speaks exclusively to ZeroApi. There is no direct database access from mobile clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resident App  ──┐
                ├──▶  ZeroApi (REST)  ──▶  PostgreSQL
Society App   ──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Visitor management
&lt;/h3&gt;

&lt;p&gt;Residents can pre-register expected visitors — family, friends, service providers — generating a one-time approval code. When the visitor arrives, the guard scans or enters the code and the resident receives an instant push notification. Entry is logged with a timestamp and a photo if the guard's device has a camera.&lt;/p&gt;

&lt;p&gt;Unregistered visitors trigger a gate-hold: the guard pings the resident in-app to approve or deny. No calls needed, no delays beyond resident response time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resident directory and flat management
&lt;/h3&gt;

&lt;p&gt;Each flat is linked to one or more residents. Owners can register family members, domestic staff, and vehicles under their flat. The society admin controls the directory and can freeze or transfer flat ownership during handovers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complaints and maintenance
&lt;/h3&gt;

&lt;p&gt;Residents raise maintenance tickets through the app. The committee receives them in the Society app, assigns them, and can update status (Open → In Progress → Resolved). Residents get notified at each stage. The full history is retained per flat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Announcements
&lt;/h3&gt;

&lt;p&gt;Society admins send announcements to all residents or specific wings/blocks. Push notifications ensure delivery without relying on everyone being in a WhatsApp group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and access control
&lt;/h2&gt;

&lt;p&gt;ZeroApi uses JWT-based authentication with role scopes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resident&lt;/td&gt;
&lt;td&gt;Own flat data, visitor approvals, complaints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guard&lt;/td&gt;
&lt;td&gt;Visitor entry/exit, resident lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;td&gt;Full society data, flat management, reports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Guards cannot see resident PII beyond what is needed to verify a visitor. Admins are scoped to their own society — the platform is multi-tenant at the society level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Native?
&lt;/h2&gt;

&lt;p&gt;Both mobile apps share a significant portion of logic — API clients, push notification handling, form components, navigation patterns. React Native allowed the team to maintain one codebase per app while shipping native performance on both iOS and Android.&lt;/p&gt;

&lt;p&gt;Expo was used during early prototyping for rapid iteration. The production builds are bare React Native for full control over native modules, especially around camera access and background push handling.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vehicle tracking&lt;/strong&gt; — ANPR camera integration for automatic vehicle entry/exit logging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; — maintenance dues collection with UPI/card support, directly in the resident app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visitor history export&lt;/strong&gt; — PDF/CSV reports for committee audits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web portal&lt;/strong&gt; — a browser-based admin dashboard for society committees who prefer desktop&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;GateKeeper Zero is a product built by &lt;a href="https://www.cadnative.com" rel="noopener noreferrer"&gt;Cadnative&lt;/a&gt;. If you're managing a housing society and want to modernise your operations, &lt;a href="https://www.cadnative.com#contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cadnative.com/blog/introducing-gatekeeper-zero" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gatekeeperzero</category>
      <category>reactnative</category>
      <category>product</category>
    </item>
    <item>
      <title>Parametric Furniture Design with IMOS IX</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 07:48:42 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/parametric-furniture-design-with-imos-ix-22cf</link>
      <guid>https://dev.to/akshay_nikhare/parametric-furniture-design-with-imos-ix-22cf</guid>
      <description>&lt;p&gt;IMOS IX is the leading CAD/CAM software for the furniture manufacturing industry. It integrates directly with woodworking CNC machines and enables fully parametric product design — meaning a single model can produce thousands of variants without manual rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is parametric design?
&lt;/h2&gt;

&lt;p&gt;Traditional CAD modelling is "static" — you draw a specific cabinet at specific dimensions. Parametric design is different: you define relationships and rules, and the software derives geometry automatically.&lt;/p&gt;

&lt;p&gt;In IMOS IX this means you can describe a wardrobe once and then configure it for any width, height, number of doors, or internal fitment — all without touching the underlying model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key IMOS IX concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Article variants
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;article&lt;/strong&gt; in IMOS IX is a product definition with parameters exposed as inputs (width, height, depth, material, etc.). The article script calculates part dimensions, materials, and CNC programmes from those inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Form editors
&lt;/h3&gt;

&lt;p&gt;IMOS IX ships with a visual &lt;strong&gt;form editor&lt;/strong&gt; that lets non-technical staff configure articles through a UI. You define which parameters are visible, their labels, validation rules, and dependencies between fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  CNC post-processing
&lt;/h3&gt;

&lt;p&gt;Once a variant is configured, IMOS IX generates machine data (DXF, MPR, or proprietary formats) for the CNC router. This removes manual measurement steps and dramatically reduces cutting errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  A typical workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer order → Configurator UI → IMOS IX article → Part list + CNC files → Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Customer selects dimensions and finish in a web configurator.&lt;/li&gt;
&lt;li&gt;IMOS IX evaluates the article script and produces a complete bill of materials.&lt;/li&gt;
&lt;li&gt;CNC files are sent directly to the machine — no manual intervention.&lt;/li&gt;
&lt;li&gt;Cut parts arrive pre-labelled for assembly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Integrating IMOS IX with an online shop
&lt;/h2&gt;

&lt;p&gt;One of our most common projects is connecting IMOS IX to a Shopware store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer configures a piece of furniture in the Shopware storefront.&lt;/li&gt;
&lt;li&gt;On order confirmation, Shopware calls a middleware API.&lt;/li&gt;
&lt;li&gt;The middleware passes parameters to IMOS IX and retrieves the generated files.&lt;/li&gt;
&lt;li&gt;Files are attached to the production order in the ERP system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This end-to-end automation typically reduces order-to-production time from days to hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does IMOS IX make sense?
&lt;/h2&gt;

&lt;p&gt;IMOS IX is worth the investment when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You offer configured-to-order (CTO) products.&lt;/li&gt;
&lt;li&gt;You have more than ~50 CNC-cut orders per month.&lt;/li&gt;
&lt;li&gt;Variant management is creating errors or bottlenecks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For smaller operations, a simpler cut-list tool may be sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talk to us
&lt;/h2&gt;

&lt;p&gt;We have built IMOS IX integrations for clients across Europe. If you're evaluating parametric design software or need help with an existing implementation, &lt;a href="https://www.cadnative.com/#contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cadnative.com/blog/imos-ix-parametric-design" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>imosix</category>
      <category>parametricdesign</category>
      <category>manufacturing</category>
    </item>
    <item>
      <title>Getting Started with Shopware 6 Development</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 07:48:22 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/getting-started-with-shopware-6-development-15fc</link>
      <guid>https://dev.to/akshay_nikhare/getting-started-with-shopware-6-development-15fc</guid>
      <description>&lt;p&gt;Shopware 6 is a modern, API-first e-commerce platform built on Symfony and Vue.js. If you're coming from Shopware 5 or another platform, the architecture shift can feel steep — but once it clicks, it's powerful and highly extensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the architecture
&lt;/h2&gt;

&lt;p&gt;Shopware 6 is split into two main areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core&lt;/strong&gt; — the Symfony-based PHP backend that handles business logic, APIs, and the administration panel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storefront&lt;/strong&gt; — a Twig-based frontend layer with optional headless API access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most customisation happens through &lt;strong&gt;plugins&lt;/strong&gt;. A plugin can override templates, add new API endpoints, register event listeners, or inject custom services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a local environment
&lt;/h2&gt;

&lt;p&gt;The fastest way to get started is with the official Docker setup:&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/shopware/shopware.git
&lt;span class="nb"&gt;cd &lt;/span&gt;shopware
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once running, access the admin at &lt;code&gt;http://localhost/admin&lt;/code&gt; and the storefront at &lt;code&gt;http://localhost&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating your first plugin
&lt;/h2&gt;

&lt;p&gt;Shopware provides a CLI tool to scaffold plugins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/console plugin:create MyPlugin
php bin/console plugin:install &lt;span class="nt"&gt;--activate&lt;/span&gt; MyPlugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a &lt;code&gt;src/MyPlugin.php&lt;/code&gt; entry point and a &lt;code&gt;composer.json&lt;/code&gt;. From here you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register &lt;strong&gt;services&lt;/strong&gt; in &lt;code&gt;Resources/config/services.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Override &lt;strong&gt;templates&lt;/strong&gt; in &lt;code&gt;Resources/views/storefront/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;migration files&lt;/strong&gt; in &lt;code&gt;src/Migration/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Theme customisation
&lt;/h2&gt;

&lt;p&gt;Themes in Shopware 6 are also plugins. They use SCSS for styling and can expose configuration variables that shop owners can control via the admin UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Resources/theme.json --&amp;gt;&lt;/span&gt;
{
  "name": "MyTheme",
  "author": "Cadnative",
  "views": ["@Storefront", "@Plugins", "@MyTheme"],
  "style": ["app/storefront/src/scss/overrides.scss"],
  "config": {
    "fields": {
      "brand-color": { "label": "Brand colour", "type": "color", "value": "#1B5A6B" }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enable &lt;strong&gt;HTTP cache&lt;/strong&gt; in production — Shopware has a built-in reverse proxy layer.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Elasticsearch&lt;/strong&gt; for product search at scale.&lt;/li&gt;
&lt;li&gt;Profile slow queries with the Symfony profiler (&lt;code&gt;/_profiler&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Leverage &lt;strong&gt;Sales Channel API&lt;/strong&gt; for headless storefronts instead of rendering Twig server-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read the &lt;a href="https://developer.shopware.com" rel="noopener noreferrer"&gt;official developer docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Explore the &lt;a href="https://github.com/shopware" rel="noopener noreferrer"&gt;Shopware GitHub repos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reach out to us at &lt;a href="https://www.cadnative.com" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt; if you need expert help&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cadnative.com/blog/getting-started-with-shopware" rel="noopener noreferrer"&gt;cadnative.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopware</category>
      <category>ecommerce</category>
      <category>php</category>
    </item>
    <item>
      <title>FireViston TV: Android App &amp; Streaming Server for the Living Room</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:23:17 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/fireviston-tv-android-app-streaming-server-for-the-living-room-48od</link>
      <guid>https://dev.to/akshay_nikhare/fireviston-tv-android-app-streaming-server-for-the-living-room-48od</guid>
      <description>&lt;p&gt;&lt;strong&gt;Live Project:&lt;/strong&gt; &lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;tv.cadnative.com&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is FireViston TV?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FireViston TV&lt;/strong&gt; is a full-stack streaming solution designed for the modern living room. It consists of two parts that work in tight coordination:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;FireViston TV Android App&lt;/strong&gt; — a native Android/Android TV application that delivers a polished, remote-friendly viewing experience. &lt;a href="https://github.com/akshaynikhare/FireVisionIPTV/releases" rel="noopener noreferrer"&gt;https://github.com/akshaynikhare/FireVisionIPTV/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FireViston TV Server&lt;/strong&gt; — the backend streaming infrastructure  that powers content delivery, user management, and playback control hosted at &lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;tv.cadnative.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, they form a complete, self-contained streaming platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android App
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Built for the 10-Foot Experience
&lt;/h3&gt;

&lt;p&gt;The FireViston Android app is designed for television screens — large fonts, d-pad navigation, and a layout that works from across the room. No pinching, no scrolling hunts, no mobile-style UI crammed onto a 55-inch display.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smooth Playback
&lt;/h3&gt;

&lt;p&gt;Video streaming requires more than just playing a file. FireViston handles adaptive bitrate streaming, buffer management, and codec compatibility to ensure smooth playback across the broadest range of Android TV devices — from budget sticks to high-end smart TVs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Discovery
&lt;/h3&gt;

&lt;p&gt;A clean, browsable content grid lets users find what they want quickly. Categories, search, and a "continue watching" row reduce friction from intent to playback.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Reliable Infrastructure
&lt;/h3&gt;

&lt;p&gt;The FireViston server at &lt;strong&gt;&lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;tv.cadnative.com&lt;/a&gt;&lt;/strong&gt; manages content ingestion, transcoding pipelines, and delivery. It's built to handle concurrent streams without degrading quality for any individual viewer.&lt;/p&gt;

&lt;h3&gt;
  
  
  API-Driven Architecture
&lt;/h3&gt;

&lt;p&gt;The Android app communicates with the server through a REST API, making the backend flexible enough to support additional clients — web players, other mobile platforms — without rewriting core logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  User &amp;amp; Session Management
&lt;/h3&gt;

&lt;p&gt;Account creation, authentication, playback progress sync, and device management all happen server-side. Users can pick up on any device exactly where they left off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Custom Streaming Stack?
&lt;/h2&gt;

&lt;p&gt;Off-the-shelf streaming platforms come with constraints: content policies, revenue sharing, limited branding control, and dependence on third-party availability.&lt;/p&gt;

&lt;p&gt;FireViston TV gives full control — over the content, the experience, and the infrastructure. For operators who need that ownership, it's the right choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get FireViston TV
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android App:&lt;/strong&gt; Available for Android and Android TV devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming Server:&lt;/strong&gt; &lt;a href="https://tv.cadnative.com/" rel="noopener noreferrer"&gt;tv.cadnative.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>fireviston</category>
      <category>androidtv</category>
      <category>streaming</category>
      <category>server</category>
    </item>
    <item>
      <title>How a Single beforeEach Killed Our CI for 36 Hours</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:23:10 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/how-a-single-beforeeach-killed-our-ci-for-36-hours-1fda</link>
      <guid>https://dev.to/akshay_nikhare/how-a-single-beforeeach-killed-our-ci-for-36-hours-1fda</guid>
      <description>&lt;p&gt;Six failed CI runs. Thirty-six hours of GitHub Actions time. Every run timing out at exactly the 6-hour limit.&lt;/p&gt;

&lt;p&gt;The culprit was one line in &lt;code&gt;tests/setup.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;We were building a multi-tenant platform with a PostgreSQL backend — around 76 database models handling everything from user accounts and billing to visitor logs and real-time notifications. The test suite had grown to roughly 1,140 test cases across 36 files.&lt;/p&gt;

&lt;p&gt;Standard stuff. CI ran on every PR. Tests passed locally. And then one day, CI just... never finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anti-Pattern
&lt;/h2&gt;

&lt;p&gt;Here's what the test setup looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/setup.js&lt;/span&gt;
&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tableNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTableNames&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 76 tables&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sequelize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`TRUNCATE TABLE &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tableNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; CASCADE;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent was clean isolation — every test starts with a blank slate. Reasonable in theory. Catastrophic in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math
&lt;/h2&gt;

&lt;p&gt;Do the multiplication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;76 tables × 1,140 tests = 86,640 TRUNCATE operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;TRUNCATE TABLE ... CASCADE&lt;/code&gt; is not a cheap operation. PostgreSQL has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Acquire exclusive locks on all referenced tables&lt;/li&gt;
&lt;li&gt;Walk the foreign key graph to find dependent tables&lt;/li&gt;
&lt;li&gt;Truncate each in dependency order&lt;/li&gt;
&lt;li&gt;Release locks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a moderately complex schema where most tables reference others (users → societies → members → invoices → payments → ...), a single &lt;code&gt;TRUNCATE ... CASCADE&lt;/code&gt; on a central table can fan out into dozens of implicit truncations.&lt;/p&gt;

&lt;p&gt;Multiply that by 86,640 and you have a test suite that will never complete within any reasonable timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Wasn't Caught Sooner
&lt;/h2&gt;

&lt;p&gt;Two reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It used to be fast.&lt;/strong&gt; When the suite had 50 tests and 20 tables, this pattern worked fine. &lt;code&gt;50 × 20 = 1,000&lt;/code&gt; truncations — uncomfortable but survivable. Nobody noticed when the suite crossed a tipping point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Local runs used a different database state.&lt;/strong&gt; Locally, developers often ran a subset of tests with &lt;code&gt;--grep&lt;/code&gt; or file-specific runs. The full suite was only ever run on CI, and CI was slow enough that most assumed it was just a "CI is slow" problem rather than a runaway complexity issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Three changes, applied together:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Move sync to &lt;code&gt;beforeAll&lt;/code&gt;, not &lt;code&gt;beforeEach&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/setup.js — AFTER&lt;/span&gt;
&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&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;await&lt;/span&gt; &lt;span class="nx"&gt;sequelize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// once per suite, not per test&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tables are created once at the start of the suite. No teardown between tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use unique identifiers in test data
&lt;/h3&gt;

&lt;p&gt;Without per-test cleanup, tests can no longer share fixed data. The fix is to make every piece of test data unique:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BEFORE — breaks without cleanup&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createTestUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// AFTER — safe without cleanup&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createTestUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`user_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="s2"&gt;@test.com`&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timestamps and short random suffixes make collisions statistically impossible across a test run. UUIDs work too — use whatever your helpers already produce.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Add CI timeout safety nets
&lt;/h3&gt;

&lt;p&gt;The default GitHub Actions job timeout is 6 hours. That's a very long time to wait before learning something is wrong. Set explicit timeouts on test steps:&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="pi"&gt;-&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;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
  &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a runaway suite fails in 15 minutes instead of 6 hours. You get the signal fast and spend less Actions budget on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&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;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TRUNCATE operations&lt;/td&gt;
&lt;td&gt;86,640&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test suite duration&lt;/td&gt;
&lt;td&gt;6+ hours (timeout)&lt;/td&gt;
&lt;td&gt;~3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI failure signal&lt;/td&gt;
&lt;td&gt;After 6 hours&lt;/td&gt;
&lt;td&gt;After 15 minutes max&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Transaction Rollback Is Better
&lt;/h2&gt;

&lt;p&gt;If your tests don't create their own transactions internally, wrapping each test in a transaction and rolling back is faster than &lt;code&gt;TRUNCATE&lt;/code&gt; and safer than the unique-ID approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sequelize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&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;await&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&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;This works well for simpler schemas. We couldn't use it because several of our tested code paths opened their own transactions internally, which can't be nested without explicit savepoint support. The unique-ID approach was the safer choice for our case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Underlying Lesson
&lt;/h2&gt;

&lt;p&gt;Test isolation strategies don't scale linearly — they scale with &lt;code&gt;tests × tables&lt;/code&gt;. A pattern that works at 50 tests and 20 tables can fail spectacularly at 1,000 tests and 76 tables.&lt;/p&gt;

&lt;p&gt;If your test suite is growing and CI is getting slower, check your setup/teardown strategy before assuming you need faster hardware.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>testing</category>
      <category>postgres</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>The x-tenant-id Pattern: Multi-Tenant API Without Multi-Tenant Complexity</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:23:02 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/the-x-tenant-id-pattern-multi-tenant-api-without-multi-tenant-complexity-4cn9</link>
      <guid>https://dev.to/akshay_nikhare/the-x-tenant-id-pattern-multi-tenant-api-without-multi-tenant-complexity-4cn9</guid>
      <description>&lt;p&gt;When you're building a multi-tenant SaaS, the first architectural question is usually: how do you keep tenant data isolated? The options range from separate databases per tenant (maximum isolation, maximum cost) to a shared database with row-level filtering (minimum cost, more careful coding required).&lt;/p&gt;

&lt;p&gt;But there's an equally important question that gets less attention: &lt;strong&gt;how does your API know which tenant context a request belongs to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post covers a pattern we've used in production: a custom request header for tenant scoping, combined with JWT authentication. Simple to implement, easy to audit, and flexible enough to support multi-tenant access from a single user account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Common Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Subdomain-based (&lt;code&gt;tenant.yourdomain.com&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The tenant is encoded in the hostname. Each subdomain routes to the same backend, which extracts the tenant from the &lt;code&gt;Host&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; Intuitive, visible in the URL.&lt;br&gt;
&lt;strong&gt;Bad:&lt;/strong&gt; Requires wildcard TLS certs, more complex DNS setup, awkward in development, doesn't work for mobile API clients the same way.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. URL path-based (&lt;code&gt;/api/tenants/{tenantId}/...&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The tenant identifier is part of every route path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; RESTful, self-documenting.&lt;br&gt;
&lt;strong&gt;Bad:&lt;/strong&gt; Bloats all route definitions, requires every endpoint to include the tenant segment, makes API versioning messier.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Header-based (&lt;code&gt;x-tenant-id: &amp;lt;id&amp;gt;&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;A custom header carries the tenant context. Routes stay clean. The tenant scope is resolved in middleware before the handler runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; Routes stay simple, middleware handles scoping uniformly, works well with JWT auth, easy to test.&lt;br&gt;
&lt;strong&gt;Bad:&lt;/strong&gt; Less visible (the tenant isn't in the URL), requires clients to always include the header.&lt;/p&gt;

&lt;p&gt;We use the header approach.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;The API accepts two forms of auth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A JWT token in the &lt;code&gt;Authorization&lt;/code&gt; header — identifies &lt;em&gt;who&lt;/em&gt; is making the request&lt;/li&gt;
&lt;li&gt;A tenant ID in the &lt;code&gt;x-tenant-id&lt;/code&gt; header — identifies &lt;em&gt;on behalf of which tenant&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/members
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
x-tenant-id: tenant_01GZ8K3X7Y
Content-Type: application/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Middleware
&lt;/h3&gt;

&lt;p&gt;Auth middleware runs first and validates the JWT. Tenant middleware runs second and validates the &lt;code&gt;x-tenant-id&lt;/code&gt; against the authenticated user's permitted tenants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware/requireAuth.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBearerToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware/requireTenantContext.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-tenant-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-tenant-id header required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify the authenticated user has access to this tenant&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;Route handlers then have &lt;code&gt;req.tenantId&lt;/code&gt; and &lt;code&gt;req.role&lt;/code&gt; available. All database queries in those handlers include &lt;code&gt;where: { tenantId: req.tenantId }&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route Registration
&lt;/h3&gt;

&lt;p&gt;Tenant-scoped routes apply both middlewares. Public routes (auth endpoints, health checks) apply neither:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Public&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loginHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;healthHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Tenant-scoped&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/members&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireTenantContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;membersRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/invoices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireTenantContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoicesRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireTenantContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settingsRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Middleware is applied at the router level, not per-handler. New routes under a tenant-scoped prefix automatically inherit the context without any extra work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Tenant Access From One Account
&lt;/h2&gt;

&lt;p&gt;The header pattern makes something else easy: a single user account accessing multiple tenants.&lt;/p&gt;

&lt;p&gt;A super-admin or management tool needs to query across tenants or switch context without re-authenticating. With the header pattern this is trivial — issue one JWT for the user, then pass different &lt;code&gt;x-tenant-id&lt;/code&gt; values per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Management dashboard switching tenant context&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchMembersForTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/members&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-tenant-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With subdomain or path-based approaches, the same scenario requires different base URLs or duplicated route structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Defense-in-Depth: Row-Level Security
&lt;/h2&gt;

&lt;p&gt;The header + middleware pattern handles application-layer tenant isolation. For an additional layer at the database level, PostgreSQL's Row-Level Security can enforce isolation even if a bug in application code omits a &lt;code&gt;tenantId&lt;/code&gt; filter:&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="c1"&gt;-- Policy: users can only see rows belonging to their current tenant&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;
  &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.current_tenant_id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the start of each request, set the current tenant on the DB connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$executeRaw&lt;/span&gt;&lt;span class="s2"&gt;`SELECT set_config('app.current_tenant_id', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, true)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now even a query that forgets &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; returns empty results instead of leaking data. The middleware is the first line of defense; RLS is the backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs to Know Before Adopting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Clients must always send the header.&lt;/strong&gt; This is a discipline requirement. Forgetting the header returns a 400, which is fast to debug, but it's a paper cut during initial integration. Document it clearly and consider a helpful error message: &lt;code&gt;"x-tenant-id header is required for this endpoint. See docs for details."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The header is visible in logs.&lt;/strong&gt; Tenant IDs aren't secrets — they're identifiers — but make sure your log sanitization rules treat them consistently with other metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switching tenants mid-session is application-layer logic.&lt;/strong&gt; The API doesn't know or care about "the current tenant" in session state. The client always tells the API which tenant context to use. This is explicit, which is good, but requires the client to manage that state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;x-tenant-id&lt;/code&gt; header pattern is not glamorous, but it's effective. Routes stay clean. Middleware handles scoping uniformly. A single JWT works across multiple tenant contexts. And the pattern composes naturally with database-level isolation when you need it.&lt;/p&gt;

&lt;p&gt;For most multi-tenant APIs where tenants are organizations (not individual users), this pattern is worth considering before reaching for the subdomain or path-based alternatives.&lt;/p&gt;

</description>
      <category>multitenancy</category>
      <category>apidesign</category>
      <category>node</category>
      <category>express</category>
    </item>
    <item>
      <title>onPreviewKeyEvent vs onKeyEvent on Android TV: A Subtle D-Pad Bug</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:22:55 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/onpreviewkeyevent-vs-onkeyevent-on-android-tv-a-subtle-d-pad-bug-3c7d</link>
      <guid>https://dev.to/akshay_nikhare/onpreviewkeyevent-vs-onkeyevent-on-android-tv-a-subtle-d-pad-bug-3c7d</guid>
      <description>&lt;p&gt;The bug report was simple: long-pressing the d-pad center button on a channel card should toggle the favorite — it wasn't working reliably. On some devices it fired once and stopped. On others it didn't fire at all on long-press.&lt;/p&gt;

&lt;p&gt;The fix was changing &lt;code&gt;onKeyEvent&lt;/code&gt; to &lt;code&gt;onPreviewKeyEvent&lt;/code&gt; in one Composable. The reason why is worth understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: Two Event Handlers in Compose
&lt;/h2&gt;

&lt;p&gt;Jetpack Compose exposes two modifier-level hooks for key input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onKeyEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;keyEvent&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPreviewKeyEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;keyEvent&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They sound equivalent. They're not. The difference is &lt;strong&gt;where they sit in the event propagation chain&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Android TV Routes D-Pad Events
&lt;/h2&gt;

&lt;p&gt;When a user presses a key on a TV remote, Android routes the event through a dispatch tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Activity
  └─ ViewGroup (root)
       └─ FocusedComposable
            └─ Child Composables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event travels &lt;strong&gt;down&lt;/strong&gt; first (capture phase), then &lt;strong&gt;up&lt;/strong&gt; (bubble phase):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture (top → focused node):&lt;/strong&gt; &lt;code&gt;onPreviewKeyEvent&lt;/code&gt; handlers fire here, outermost first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bubble (focused node → top):&lt;/strong&gt; &lt;code&gt;onKeyEvent&lt;/code&gt; handlers fire here, innermost first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;onPreviewKeyEvent&lt;/code&gt; is the capture phase. &lt;code&gt;onKeyEvent&lt;/code&gt; is the bubble phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Long-Press
&lt;/h2&gt;

&lt;p&gt;Android TV handles long-press recognition at the framework level. When you hold the d-pad center button:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;KeyEvent.ACTION_DOWN&lt;/code&gt; fires immediately.&lt;/li&gt;
&lt;li&gt;If the key is held, the framework generates repeated &lt;code&gt;ACTION_DOWN&lt;/code&gt; events at the key repeat rate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ACTION_UP&lt;/code&gt; fires when the button is released.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The long-press callback that Compose's focus system uses for "confirm" actions (select, activate) consumes &lt;code&gt;ACTION_DOWN&lt;/code&gt; during the bubble phase — specifically to prevent the holding action from also triggering the tap action.&lt;/p&gt;

&lt;p&gt;When the &lt;code&gt;ChannelCard&lt;/code&gt; had a click handler wired for the primary action and &lt;code&gt;onKeyEvent&lt;/code&gt; for the long-press toggle, the click handler's bubble-phase consumption of &lt;code&gt;ACTION_DOWN&lt;/code&gt; was racing with the long-press handler. On some devices the click handler won, swallowing the event before the long-press code ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onKeyEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;keyEvent&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionCenter&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KeyEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyDown&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLongPress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onToggleFavorite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;false&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;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPreviewKeyEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;keyEvent&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionCenter&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KeyEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyDown&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLongPress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onToggleFavorite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;false&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;&lt;code&gt;onPreviewKeyEvent&lt;/code&gt; intercepts the event during the capture phase — before any child or peer handler gets a chance to consume it. The long-press fires cleanly, and returning &lt;code&gt;true&lt;/code&gt; stops the event from propagating further, so the normal click action doesn't also trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&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;Reacting to a key after children have had first chance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onKeyEvent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intercepting a key before children or peer handlers see it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onPreviewKeyEvent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global shortcuts that should always fire regardless of focus&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;onPreviewKeyEvent&lt;/code&gt; on a parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input that should only fire when no child claimed it&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;onKeyEvent&lt;/code&gt; on a parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-press that conflicts with click on the same node&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onPreviewKeyEvent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb: if a click handler and a key handler live on the same composable and the key handler is for a long-press or a secondary action, use &lt;code&gt;onPreviewKeyEvent&lt;/code&gt; to get in before the click machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on &lt;code&gt;isLongPress&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;isLongPress&lt;/code&gt; property on &lt;code&gt;KeyEvent&lt;/code&gt; in Compose isn't always reliable across all Android TV hardware. A more robust approach uses elapsed time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;keyDownTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;

&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPreviewKeyEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;keyEvent&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionCenter&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KeyEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyDown&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyDownTime&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;keyDownTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;held&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;keyDownTime&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;onToggleFavorite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;keyDownTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;
                &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionCenter&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;keyEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KeyEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyUp&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;keyDownTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;
            &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&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;This measures elapsed hold time manually, which is consistent across the wide range of Android TV hardware — from budget HDMI sticks to mid-range smart TVs — where key repeat rates and long-press thresholds vary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onKeyEvent&lt;/code&gt; and &lt;code&gt;onPreviewKeyEvent&lt;/code&gt; are not interchangeable. If you're building interactive cards on Android TV and a long-press isn't firing reliably, the cause is almost certainly event consumption order. Move to &lt;code&gt;onPreviewKeyEvent&lt;/code&gt; and intercept before the bubble phase claims the event.&lt;/p&gt;

</description>
      <category>androidtv</category>
      <category>jetpackcompose</category>
      <category>dpad</category>
      <category>inputhandling</category>
    </item>
    <item>
      <title>Mono-Repo + Multi-Repo: How We Structured 6 Apps Across 4 Repositories</title>
      <dc:creator>Akshay Nikhare</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:22:45 +0000</pubDate>
      <link>https://dev.to/akshay_nikhare/mono-repo-multi-repo-how-we-structured-6-apps-across-4-repositories-39bi</link>
      <guid>https://dev.to/akshay_nikhare/mono-repo-multi-repo-how-we-structured-6-apps-across-4-repositories-39bi</guid>
      <description>&lt;p&gt;Most teams treat "monorepo vs multi-repo" as a binary choice. Pick one, commit, move on. We ended up with a hybrid, and it turned out to be the right call — not out of indecision, but because our apps have genuinely different deployment and ownership characteristics.&lt;/p&gt;

&lt;p&gt;Here's what we built, why, and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The System
&lt;/h2&gt;

&lt;p&gt;The platform consists of six applications:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Primary Users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REST API backend&lt;/td&gt;
&lt;td&gt;Node.js + TypeScript&lt;/td&gt;
&lt;td&gt;— (consumed by all apps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Society dashboard&lt;/td&gt;
&lt;td&gt;React web app&lt;/td&gt;
&lt;td&gt;Society managers, admins, accountants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company admin panel&lt;/td&gt;
&lt;td&gt;React web app&lt;/td&gt;
&lt;td&gt;Internal operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing website&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resident mobile app&lt;/td&gt;
&lt;td&gt;React Native (Expo)&lt;/td&gt;
&lt;td&gt;Residents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guard mobile app&lt;/td&gt;
&lt;td&gt;React Native (Expo)&lt;/td&gt;
&lt;td&gt;Security personnel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All six apps talk to the same API. But they have very different deployment cycles, team ownership, and testing requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Structure: 4 Repositories
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo: main-platform (monorepo)
├── api/           — Express + Prisma backend
├── web-society/   — Society dashboard
├── web-admin/     — Company admin panel
└── web-marketing/ — Marketing site

repo: mobile-resident  — Resident app (React Native)
repo: mobile-guard     — Guard app (React Native)
repo: mobile-staff     — Society staff mobile app (React Native)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web apps and the API live together in one monorepo. The three mobile apps each have their own repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Split Mobile From Web?
&lt;/h2&gt;

&lt;p&gt;The driving factor was &lt;strong&gt;deployment cadence and review process&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Web apps deploy on push — merge to main, CI builds, CDN updated within minutes. The feedback loop is fast, rollbacks are instant, and there's no approval gate between code and production.&lt;/p&gt;

&lt;p&gt;Mobile apps go through app store review. A release cycle includes building a release APK, submitting to Google Play (and Apple App Store), waiting for review, and then a staged rollout. The cadence is measured in days, not minutes. Mistakes are expensive to reverse — a bad release means submitting a patch, waiting again, and potentially having a broken version live for days.&lt;/p&gt;

&lt;p&gt;Given that difference, mobile releases warrant a separate release workflow, separate versioning (&lt;code&gt;release-please&lt;/code&gt; per repo), separate EAS build config, and a dedicated changelog. Bundling mobile with web would mean mobile releases dragging along unrelated web changes and making the release notes hard to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate repos give each mobile app its own clean release history.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Keep the Backend and Web Apps Together?
&lt;/h2&gt;

&lt;p&gt;Because they change together constantly.&lt;/p&gt;

&lt;p&gt;Most features touch both the API and a web frontend. A new billing feature means a new API endpoint, a new React page, and updates to both. With the API and web apps in the same repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One PR covers the full change&lt;/li&gt;
&lt;li&gt;The CI pipeline validates everything together&lt;/li&gt;
&lt;li&gt;Reviewing a PR gives you the complete picture&lt;/li&gt;
&lt;li&gt;No cross-repo issue tracking for basic feature work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If they were in separate repos, every feature PR would spawn linked PRs across two repositories — more coordination overhead for changes that are logically one unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cross-Repo Challenge
&lt;/h2&gt;

&lt;p&gt;The seam between repos is where friction lives.&lt;/p&gt;

&lt;p&gt;When a new API endpoint or a modified contract affects mobile apps, you get cross-repo work: one PR in the main platform repo, one or more PRs in the mobile repos. We handle this with explicit cross-linking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parent issue lives in the main repo&lt;/li&gt;
&lt;li&gt;Each mobile repo gets a child issue referencing back&lt;/li&gt;
&lt;li&gt;PRs on both sides link to the same parent issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works, but it requires discipline. The worst version of this is when someone merges an API change without checking whether the mobile apps need a corresponding update. To guard against that, breaking API changes go through a separate review step that explicitly asks: "which mobile app versions does this affect, and are those PRs in flight?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch Protection Across All Repos
&lt;/h2&gt;

&lt;p&gt;All four repos have identical branch protection on &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No direct pushes&lt;/li&gt;
&lt;li&gt;At least one approving review required&lt;/li&gt;
&lt;li&gt;CI must pass before merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is non-negotiable. With a multi-repo setup, it's easy for protection rules to diverge between repos. Keeping them identical means the same habits apply regardless of which repo you're working in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Dependencies and Drift
&lt;/h2&gt;

&lt;p&gt;The main risk with multi-repo mobile apps is dependency drift. Each mobile repo has its own &lt;code&gt;package.json&lt;/code&gt;. If &lt;code&gt;main-platform&lt;/code&gt; bumps a shared utility, the mobile repos don't automatically follow.&lt;/p&gt;

&lt;p&gt;Our mitigation: the Expo SDK version and key shared dependencies (React Native, TypeScript, Babel config) are pinned to the same versions across all mobile repos. We check this in a periodic sync script rather than trying to enforce it automatically — automatic enforcement across repos adds tooling complexity we haven't needed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Better automation for cross-repo issues.&lt;/strong&gt; Manually linking parent and child issues works until someone forgets. A GitHub Action that opens draft child issues in mobile repos when a label is applied to a main-repo issue would remove that failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared TypeScript types package.&lt;/strong&gt; API response types are currently duplicated between the backend, web apps, and mobile apps. A shared &lt;code&gt;@platform/types&lt;/code&gt; internal package published to a private registry (or referenced via workspace) would make type drift detectable at compile time. We've planned this but haven't needed it badly enough to prioritize it yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document the structure early.&lt;/strong&gt; The hybrid structure is intuitive once you understand the reasoning, but onboarding a new contributor requires explaining why mobile lives in separate repos. A &lt;code&gt;REPOS.md&lt;/code&gt; at the root of the main repo — linking out and explaining the structure — saves the first 10 minutes of every onboarding conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Characteristic&lt;/th&gt;
&lt;th&gt;Monorepo (web + API)&lt;/th&gt;
&lt;th&gt;Multi-repo (mobile)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment cadence&lt;/td&gt;
&lt;td&gt;Continuous (minutes)&lt;/td&gt;
&lt;td&gt;Store review (days)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical change scope&lt;/td&gt;
&lt;td&gt;Multi-file, multi-layer&lt;/td&gt;
&lt;td&gt;Self-contained per app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release notes&lt;/td&gt;
&lt;td&gt;Combined&lt;/td&gt;
&lt;td&gt;Per app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR complexity&lt;/td&gt;
&lt;td&gt;High — covers whole feature&lt;/td&gt;
&lt;td&gt;Focused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-team coordination&lt;/td&gt;
&lt;td&gt;Implicit (same repo)&lt;/td&gt;
&lt;td&gt;Explicit (issue links)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monorepos reduce coordination overhead for tightly coupled code. Multi-repo reduces noise for code that releases independently. The answer isn't one or the other — it's matching your repo structure to your actual deployment and ownership boundaries.&lt;/p&gt;

</description>
      <category>monorepo</category>
      <category>multirepo</category>
      <category>cicd</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
