<?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: Jani Hautakangas</title>
    <description>The latest articles on DEV Community by Jani Hautakangas (@janihau).</description>
    <link>https://dev.to/janihau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3635802%2F30618e2e-6e8c-4fa9-9462-345be1385224.png</url>
      <title>DEV Community: Jani Hautakangas</title>
      <link>https://dev.to/janihau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janihau"/>
    <language>en</language>
    <item>
      <title>How to Build an Enterprise Browser — Branding</title>
      <dc:creator>Jani Hautakangas</dc:creator>
      <pubDate>Tue, 26 May 2026 16:22:01 +0000</pubDate>
      <link>https://dev.to/janihau/how-to-build-an-enterprise-browser-branding-3g65</link>
      <guid>https://dev.to/janihau/how-to-build-an-enterprise-browser-branding-3g65</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/janihau/how-to-build-an-enterprise-browser-571f"&gt;first post&lt;/a&gt; I sketched out what an enterprise browser is and laid the foundation for my proof-of-concept, &lt;a href="https://github.com/KodeGood/enterprise-browser-chromium" rel="noopener noreferrer"&gt;enterprise-browser-chromium&lt;/a&gt;. One bullet in that post was &lt;em&gt;“Branding: Apply custom branding to establish a unique identity.”&lt;/em&gt; In practice that one bullet means touching strings, themes, build flags, installer code, Mac bundles, Windows resource sections, and Linux desktop files. This post is dedicated to that one bullet.&lt;/p&gt;




&lt;h1&gt;
  
  
  What “branding” really is in Chromium
&lt;/h1&gt;

&lt;p&gt;Chromium ships as a generic open-source browser; Google Chrome is the proprietary product built on top of it. Anything trademarked (the Chrome logo, the “Google Chrome” name, certain installer artwork) sits behind a small set of build switches and is omitted from the public build. Branding, in source terms, is the answer to a handful of questions asked at build time: what product name to use, where icons and OS-level assets live, and what the product calls itself in the UI and installer.&lt;/p&gt;




&lt;h1&gt;
  
  
  How Chromium wires the branding switch
&lt;/h1&gt;

&lt;p&gt;The whole branding system starts with a single GN argument declared in &lt;a href="https://source.chromium.org/chromium/chromium/src/+/main:build/config/chrome_build.gni" rel="noopener noreferrer"&gt;&lt;code&gt;build/config/chrome_build.gni&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;declare_args() {
  is_chrome_branded = false
  # ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that argument flips to &lt;code&gt;true&lt;/code&gt;, Chromium’s build switches to Google’s proprietary identity. (Internally the if/else keys off a derived &lt;code&gt;is_internal_chrome_branded = is_chrome_branded &amp;amp;&amp;amp; enable_src_internal&lt;/code&gt;; the second flag pulls in Google’s &lt;code&gt;src-internal&lt;/code&gt; repo and isn’t available to public builds.) Two path variables, set in the same file, are what the rest of the build consumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (is_internal_chrome_branded) {
  branding_path_component = "google_chrome"
  branding_path_product   = "google_chrome"
} else {
  branding_path_component = "chromium"
  branding_path_product   = "chromium"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost everything else flows from those two strings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;branding_path_component&lt;/code&gt; selects the subdirectory inside &lt;code&gt;chrome/app/theme/&lt;/code&gt; for icons and artwork.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;branding_path_product&lt;/code&gt; selects the &lt;code&gt;.grd&lt;/code&gt; file used for branded strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In C++, the same switch surfaces as a &lt;code&gt;BUILDFLAG&lt;/code&gt;. &lt;code&gt;build/BUILD.gn&lt;/code&gt; generates &lt;code&gt;branding_buildflags.h&lt;/code&gt;, which gives Chromium code two flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
&lt;/span&gt;  &lt;span class="c1"&gt;// Google Chrome only&lt;/span&gt;
&lt;span class="cp"&gt;#elif BUILDFLAG(CHROMIUM_BRANDING)
&lt;/span&gt;  &lt;span class="c1"&gt;// Chromium (or any non-Google branded build)&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Grit (the string and resource format), the same switch appears as &lt;code&gt;&amp;lt;if expr="_google_chrome"&amp;gt;&lt;/code&gt;. In Java, it appears as &lt;code&gt;BuildConfig.IS_CHROME_BRANDED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A downstream browser tells the build about itself, supplies parallel assets, and the rest of the build picks them up automatically.&lt;/p&gt;




&lt;h1&gt;
  
  
  The BRANDING file
&lt;/h1&gt;

&lt;p&gt;Each brand has a plain-text &lt;code&gt;BRANDING&lt;/code&gt; file at the root of its theme directory. Enterprise Browser’s looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;COMPANY_FULLNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KodeGood&lt;/span&gt;
&lt;span class="py"&gt;COMPANY_SHORTNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KodeGood&lt;/span&gt;
&lt;span class="py"&gt;PRODUCT_FULLNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Enterprise Browser&lt;/span&gt;
&lt;span class="py"&gt;PRODUCT_SHORTNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Enterprise Browser&lt;/span&gt;
&lt;span class="py"&gt;PRODUCT_INSTALLER_FULLNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Enterprise Browser Installer&lt;/span&gt;
&lt;span class="py"&gt;PRODUCT_INSTALLER_SHORTNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Enterprise Browser Installer&lt;/span&gt;
&lt;span class="py"&gt;COPYRIGHT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Copyright @LASTCHANGE_YEAR@ Jani Hautakangas &amp;lt;jani@kodegood.com&amp;gt;. All rights reserved.&lt;/span&gt;
&lt;span class="py"&gt;MAC_BUNDLE_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;com.kodegood.EnterpriseBrowser&lt;/span&gt;
&lt;span class="py"&gt;MAC_CREATOR_CODE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Cr24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;build/util/branding.gni&lt;/code&gt; parses this file at GN time and exposes its fields as GN variables (&lt;code&gt;chrome_product_full_name&lt;/code&gt;, &lt;code&gt;chrome_mac_bundle_id&lt;/code&gt;, and friends). Those variables then flow into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Windows &lt;code&gt;.rc&lt;/code&gt; resource that decorates the EXE with version metadata,&lt;/li&gt;
&lt;li&gt;the Mac &lt;code&gt;Info.plist&lt;/code&gt; template, where &lt;code&gt;CFBundleIdentifier&lt;/code&gt; and &lt;code&gt;CFBundleName&lt;/code&gt; are substituted from these fields,&lt;/li&gt;
&lt;li&gt;the Linux installer scripts, which use the company and product names to generate &lt;code&gt;.deb&lt;/code&gt;/&lt;code&gt;.rpm&lt;/code&gt; package metadata.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The main branding topics
&lt;/h1&gt;

&lt;p&gt;The diagram below shows the parallel &lt;code&gt;enterprise_browser/&lt;/code&gt; tree in the repo. Each topic that follows maps to one or more of these paths.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Product name strings
&lt;/h2&gt;

&lt;p&gt;Chromium splits the product-name-bearing strings into two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chrome/app/chromium_strings.grd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chrome/app/google_chrome_strings.grd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are structurally identical but say &lt;em&gt;Chromium&lt;/em&gt; and &lt;em&gt;Google Chrome&lt;/em&gt; respectively. The build chooses between them through &lt;code&gt;branding_path_product&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grit_strings("branded_strings") {
  source = "${branding_path_product}_strings.grd"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern is repeated in &lt;code&gt;components/&lt;/code&gt;, where there is &lt;code&gt;components_chromium_strings.grd&lt;/code&gt; vs &lt;code&gt;components_google_chrome_strings.grd&lt;/code&gt;, and again in the settings UI via &lt;code&gt;.grdp&lt;/code&gt; partials.&lt;/p&gt;

&lt;p&gt;These three pairs exist because they live in three layers: top-level browser strings, settings UI strings, and &lt;code&gt;//components&lt;/code&gt; (which is architecturally below &lt;code&gt;//chrome&lt;/code&gt; and cannot depend on it).&lt;/p&gt;

&lt;p&gt;Doing this rename by hand isn't realistic: the English &lt;code&gt;.grd&lt;/code&gt; has hundreds of &lt;em&gt;Chromium&lt;/em&gt; mentions, multiplied by 80+ per-locale &lt;code&gt;.xtb&lt;/code&gt; files. A broader Chrome/Chromium sweep can also mangle compound terms like &lt;code&gt;ChromeOS&lt;/code&gt; and &lt;code&gt;Chromecast&lt;/code&gt;, which must survive intact. The only sane path is a script.&lt;/p&gt;

&lt;p&gt;Enterprise Browser ships an &lt;code&gt;eb l10n&lt;/code&gt; command for this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠ Caveat: a regex sweep across 80+ locales is a &lt;em&gt;placeholder&lt;/em&gt;, not a real translation. Non-Latin transliterations and idiomatic phrasing need a real translation pipeline, which this post doesn’t cover.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h2&gt;
  
  
  Themes, icons, and platform artwork
&lt;/h2&gt;

&lt;p&gt;Visual assets live in &lt;code&gt;chrome/app/theme/&lt;/code&gt;, organized in three parallel scale tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;chrome&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;/&lt;span class="n"&gt;theme&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;brand&lt;/span&gt;&amp;gt;/                   &lt;span class="c"&gt;# vector + master assets, BRANDING file
&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;/&lt;span class="n"&gt;theme&lt;/span&gt;/&lt;span class="n"&gt;default_100_percent&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;brand&lt;/span&gt;&amp;gt;/  &lt;span class="c"&gt;# raster, 1x
&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;/&lt;span class="n"&gt;theme&lt;/span&gt;/&lt;span class="n"&gt;default_200_percent&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;brand&lt;/span&gt;&amp;gt;/  &lt;span class="c"&gt;# raster, 2x
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;&amp;lt;brand&amp;gt;/&lt;/code&gt;, the build expects platform subdirectories: &lt;code&gt;mac/app.icns&lt;/code&gt;, &lt;code&gt;win/&amp;lt;brand&amp;gt;.ico&lt;/code&gt; (and a handful of file-handler icons), and &lt;code&gt;linux/product_logo_*.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chrome/app/theme/theme_resources.grd&lt;/code&gt; references the in-binary UI resources here via &lt;code&gt;${branding_path_component}&lt;/code&gt; substitution; the platform-specific bundle assets (&lt;code&gt;.icns&lt;/code&gt;, &lt;code&gt;.ico&lt;/code&gt;, Linux logos) flow in through separate platform bundle and installer rules.&lt;/p&gt;

&lt;p&gt;Enterprise Browser keeps its parallel set of these assets under &lt;code&gt;enterprise_browser/app/theme/&lt;/code&gt;; the &lt;code&gt;eb branding&lt;/code&gt; build step mirrors them into &lt;code&gt;chrome/app/theme/enterprise_browser/&lt;/code&gt; before GN runs.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The branding GN switch
&lt;/h2&gt;

&lt;p&gt;Chromium’s &lt;code&gt;chrome_build.gni&lt;/code&gt; only knows about two brands out of the box: &lt;code&gt;chromium&lt;/code&gt; and &lt;code&gt;google_chrome&lt;/code&gt;. To plug in a third, you have two options: patch the upstream file, or override the GN args before GN runs.&lt;/p&gt;

&lt;p&gt;Enterprise Browser currently takes the patch route. &lt;a href="https://github.com/KodeGood/enterprise-browser-chromium/blob/main/patches/build-config-chrome_build.gni.patch" rel="noopener noreferrer"&gt;&lt;code&gt;build-config-chrome_build.gni.patch&lt;/code&gt;&lt;/a&gt; adds a new branch to the conditional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;} else if (is_enterprise_browser) {
  branding_path_component = "enterprise_browser"
  branding_path_product   = "enterprise_browser"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and the new &lt;code&gt;is_enterprise_browser&lt;/code&gt; argument is declared in &lt;code&gt;enterprise_browser/build/enterprise_browser.gni&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;declare_args() {
  is_enterprise_browser = true
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but carries an ongoing cost. The patch's hunk context sits inside the &lt;code&gt;if / else if / else&lt;/code&gt; chain in &lt;code&gt;chrome_build.gni&lt;/code&gt;. Any time upstream touches that chain (new branches, reordering, rewritten declarations), the surrounding lines stop matching and the patch needs rebasing. The breakage is mechanical, not logical, but you still have to fix it on every uprev (each time you update to a newer Chromium version).&lt;/p&gt;

&lt;p&gt;The better long-term approach skips the patch entirely. GN args declared with &lt;code&gt;declare_args()&lt;/code&gt; can be overridden from &lt;code&gt;args.gn&lt;/code&gt;: your value wins, the default is skipped. Ship a &lt;code&gt;branding_defaults.gni&lt;/code&gt; that sets &lt;code&gt;branding_path_component&lt;/code&gt; and &lt;code&gt;branding_path_product&lt;/code&gt;, and import it from &lt;code&gt;args.gn&lt;/code&gt; before &lt;code&gt;gn gen&lt;/code&gt; runs. The upstream file stays untouched, and uprevs become noticeably less painful.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Installer strings and platform packaging
&lt;/h2&gt;

&lt;p&gt;The installer has its own product name, its own shortcut names, and on Windows it registers OS-level rules that include the product name.&lt;/p&gt;

&lt;p&gt;Chromium drives a lot of this from a Python script, &lt;a href="https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/util/prebuild/create_installer_string_rc.py" rel="noopener noreferrer"&gt;&lt;code&gt;chrome/installer/util/prebuild/create_installer_string_rc.py&lt;/code&gt;&lt;/a&gt;. It has a &lt;code&gt;MODE_SPECIFIC_STRINGS&lt;/code&gt; dictionary that, for each branding mode (&lt;code&gt;google_chrome&lt;/code&gt;, &lt;code&gt;chromium&lt;/code&gt;), lists which string IDs to emit into the installer’s resource section.&lt;/p&gt;

&lt;p&gt;Enterprise Browser patches that script (&lt;a href="https://github.com/KodeGood/enterprise-browser-chromium/blob/main/patches/chrome-installer-util-prebuild-create_installer_string_rc.py.patch" rel="noopener noreferrer"&gt;&lt;code&gt;chrome-installer-util-prebuild-create_installer_string_rc.py.patch&lt;/code&gt;&lt;/a&gt;) to add an &lt;code&gt;enterprise_browser&lt;/code&gt; mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MODE_SPECIFIC_STRINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IDS_APP_SHORTCUTS_SUBDIR_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;google_chrome&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IDS_APP_SHORTCUTS_SUBDIR_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IDS_APP_SHORTCUTS_SUBDIR_NAME_BETA&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chromium&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IDS_APP_SHORTCUTS_SUBDIR_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;enterprise_browser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IDS_APP_SHORTCUTS_SUBDIR_NAME&lt;/span&gt;&lt;span class="sh"&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="c1"&gt;# ...same shape for IDS_INBOUND_MDNS_RULE_DESCRIPTION, IDS_PRODUCT_NAME, etc.
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of detail that is invisible until you ship a Windows build, and then it becomes a long tail of small symptoms (wrong product name in the Start menu, wrong firewall rule description, etc.) that all trace back to a few entries in this dictionary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vector icons inside the UI
&lt;/h2&gt;

&lt;p&gt;Branding is not only the outside of the binary. Vector icons baked into the browser UI (the About-dialog product mark, the new tab page icon, the icon used in profile pickers) live in &lt;code&gt;.icon&lt;/code&gt; files under &lt;code&gt;components/vector_icons/&amp;lt;brand&amp;gt;/&lt;/code&gt;. Enterprise Browser ships its own &lt;code&gt;product.icon&lt;/code&gt; and &lt;code&gt;product_refresh.icon&lt;/code&gt; under &lt;code&gt;components/vector_icons/enterprise_browser/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI customization: settings layout, new tab page, splash
&lt;/h2&gt;

&lt;p&gt;Beyond names and icons, branding extends to the layout and content of user-facing surfaces too: &lt;code&gt;chrome://settings&lt;/code&gt; sections, the new tab page, the first-run welcome, splash and intro screens, the profile picker, the about page. Mechanically this is different work: editing WebUI HTML/TS/CSS under &lt;code&gt;chrome/browser/resources/&lt;/code&gt; plus native C++ that builds the settings tree.&lt;/p&gt;

&lt;p&gt;Enterprise Browser has made a start here. The intro flow, the first-run / splash onboarding sequence, and the profile picker are already customized, so the onboarding and sign-in surfaces are visibly Enterprise Browser.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The build-time mirroring tool
&lt;/h2&gt;

&lt;p&gt;Most of the above is data: strings, themes, vector icons, the BRANDING file. The thing that &lt;em&gt;applies&lt;/em&gt; them to the Chromium tree is a build step that mirrors your branding files into the right places under &lt;code&gt;src/&lt;/code&gt; before GN runs.&lt;/p&gt;

&lt;p&gt;Enterprise Browser does the same thing in &lt;a href="https://github.com/KodeGood/enterprise-browser-chromium/blob/main/tools/eb/eb.py" rel="noopener noreferrer"&gt;&lt;code&gt;tools/eb/eb.py&lt;/code&gt;&lt;/a&gt; under &lt;code&gt;cmd_branding&lt;/code&gt;. The mapping is exactly the parallel set: theme directories (master + both scale tiers), the XTB translation resources, the three string bundles, and the vector icons. Run as &lt;code&gt;eb branding&lt;/code&gt;, or implicitly via &lt;code&gt;eb sync&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Conceptually Chromium’s branding system is a single switch that cascades through dozens of subsystems: one flag, two path variables. The implementation is messier: a long tail of &lt;code&gt;#if BUILDFLAG(GOOGLE_CHROME_BRANDING)&lt;/code&gt; blocks, Grit &lt;code&gt;&amp;lt;if expr="_google_chrome"&amp;gt;&lt;/code&gt; conditionals, Java checks, and per-mode entries in build scripts. Adding a new brand means making sure all those fences understand your brand.&lt;/p&gt;

&lt;p&gt;Enterprise Browser has the foundation in place (GN wiring, theme tree, string bundles, BRANDING file, build-step mirror), but most icons and strings under those directories are still stock Chromium files copied and renamed. Most topics above are partly done at best, with branded artwork, code signing, and the updater’s own branding pipeline (&lt;code&gt;chrome/updater/branding.gni&lt;/code&gt; is its own subsystem and isn’t covered here) still ahead. Each piece is small in isolation; most have to be re-checked on every uprev.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/KodeGood/enterprise-browser-chromium" rel="noopener noreferrer"&gt;KodeGood/enterprise-browser-chromium&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>browser</category>
      <category>chromium</category>
      <category>enterprisebrowser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Build an Enterprise Browser</title>
      <dc:creator>Jani Hautakangas</dc:creator>
      <pubDate>Mon, 16 Feb 2026 18:01:28 +0000</pubDate>
      <link>https://dev.to/janihau/how-to-build-an-enterprise-browser-571f</link>
      <guid>https://dev.to/janihau/how-to-build-an-enterprise-browser-571f</guid>
      <description>&lt;p&gt;The concept of an enterprise browser was something I hadn’t paid much attention to until recently, when I found myself in discussions with teams building these systems.&lt;/p&gt;

&lt;p&gt;Having worked as a contractor for over ten years, I’ve frequently used managed browsers such as Chrome and Edge. Yet I rarely considered what actually goes into making them “enterprise-ready.” My work has primarily involved Chromium engine internals, including the content layer, Blink, and hardware integration, rather than the browser application layer itself.&lt;/p&gt;

&lt;p&gt;During those discussions, I realized how interesting enterprise browsers are, especially in safety-critical environments. I recalled multiple situations in my career where a hardened enterprise browser would have been far more practical than the complicated stacks of VPNs, VDI setups, and jump hosts required just to access secure development environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Enterprise Browser Blueprint
&lt;/h2&gt;

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

&lt;p&gt;Before tearing down the Chromium source code, we must define what “enterprise-grade” actually means. It boils down to five core pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Policy Enforcement Engine:&lt;/strong&gt; A remotely managed control plane that defines, distributes, and enforces browser behavior across all managed instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity- and Context-Aware Access Control:&lt;/strong&gt; An identity-integrated runtime that evaluates access decisions based on user identity, device posture, network context, and real-time risk signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Loss Prevention (DLP):&lt;/strong&gt; A protection layer that monitors and restricts sensitive data flows within and between web applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insights and Reporting:&lt;/strong&gt; Centralized telemetry and proactive security event reporting for visibility and auditability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Trust Engine:&lt;/strong&gt; A continuous verification model enforcing least-privilege access by validating identity and session risk throughout the lifecycle of a session. “Never Trust, Always Verify.”&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Managed Standard Browsers vs. Commercial Solutions
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Managed Standard Browsers (Chrome &amp;amp; Edge)
&lt;/h4&gt;

&lt;p&gt;These are “enterprise-light” versions of everyday browsers. They are essentially consumer Chromium binaries with management hooks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Chrome Enterprise:&lt;/strong&gt; Managed via Chrome Browser Cloud Management (&lt;strong&gt;CBCM&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Edge for Business:&lt;/strong&gt; Deeply integrated with Microsoft Entra ID and Microsoft ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy &amp;amp; DLP Capabilities:&lt;/strong&gt; Both provide an extensive set of enterprise policies, built-in data loss prevention capabilities, Safe Browsing integration, identity-based access controls, and centralized reporting through their respective management platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Connectors:&lt;/strong&gt; These are integration frameworks that allow the browser to send telemetry and content (such as file uploads or clipboard data) to third-party security providers for real-time analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Commercial Enterprise Browsers
&lt;/h4&gt;

&lt;p&gt;These are typically hardened Chromium derivatives or wrapper-based architectures designed specifically for security boundaries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Granular Policy Extensions:&lt;/strong&gt; Hyper-specific controls over browser internals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Data Loss Prevention:&lt;/strong&gt; These tools implement advanced content inspection to redact sensitive data in real-time and prevent unauthorized data movement between applications via the clipboard or file uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Last Mile” Visibility:&lt;/strong&gt; By enforcing policies within the browser runtime and renderer process, these solutions gain visibility into the DOM and user interactions that are invisible to network-level security tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control over the browser runtime:&lt;/strong&gt; The ability to modify or instrument components of the rendering engine and JavaScript execution environment when necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailored per client:&lt;/strong&gt; The entire browser lifecycle and feature set can be customized to meet the specific compliance or operational needs of a single customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build a Custom Enterprise Browser?
&lt;/h2&gt;

&lt;p&gt;n my case, this project is driven by curiosity and the fact that no open-source project currently provides a full enterprise browser stack. However, for organizations in high-stakes industries such as Defense, Intelligence, or High-Frequency Trading, the reasoning goes far deeper than curiosity.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Sovereignty Gap
&lt;/h4&gt;

&lt;p&gt;While Chrome and Edge offer a vast number of security, reporting, and policy enforcement features, they are fundamentally designed to operate within the Google or Microsoft ecosystems. For organizations with extreme security requirements, this creates a “Sovereignty Gap” that only a custom-built solution can bridge.&lt;/p&gt;

&lt;p&gt;Building a custom browser allows us to achieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;De-Google the Core:&lt;/strong&gt; Completely strip proprietary Google service dependencies that standard browsers require for background tasks, sync, and telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Services are Not Allowed:&lt;/strong&gt; Ensure that no part of the browser attempts to communicate with Google-owned endpoints, eliminating “phone-home” traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External Cloud is Forbidden:&lt;/strong&gt; Architect the management and reporting plane to exist entirely within a private, air-gapped, or sovereign cloud environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Runtime Instrumentation:&lt;/strong&gt; Inject advanced controls deep into the renderer and JavaScript engine to monitor and block malicious behavior at the instruction level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-client Hardened Runtime:&lt;/strong&gt; Tailor the specific security surface area such as disabling specific Web APIs or hardware acceleration based on the unique risk profile of a specific client or mission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability &amp;amp; Determinism:&lt;/strong&gt; Achieve full source-level auditability and deterministic builds, ensuring that every binary can be verified and every runtime decision can be traced back to a specific line of code.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Building a Custom Enterprise Browser
&lt;/h2&gt;

&lt;p&gt;Almost every enterprise browser is Chromium-based. There is a good reason for that: it provides many necessary building blocks out of the box. Even when full implementations are not available, the necessary interfaces usually are. Therefore, it is significantly more practical to build on top of Chromium than to start from scratch, especially considering that for many users, the word “browser” effectively means Chrome.&lt;/p&gt;

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

&lt;p&gt;To make it a true enterprise tool, several components must be implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud Management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device Management and Reporting Servers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure Browsing Services&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero Trust Engine&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a Chromium integration perspective, the following require extension or replacement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Service Integration:&lt;/strong&gt; Connecting the browser to the management plane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Handlers:&lt;/strong&gt; Implementing specialized handlers for proprietary security constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DLP Logic:&lt;/strong&gt; Enhanced data loss prevention mechanisms integrated into the runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Last-mile” Control:&lt;/strong&gt; Advanced controls injected deep into the Chromium engine.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementing the Foundation
&lt;/h2&gt;

&lt;p&gt;There are many approaches to implementing an enterprise browser, and there is no single correct path. It requires significant effort on both the cloud and browser sides. However, given the AI tooling available today, the barrier to entry is significantly lower than it was just a few years ago.&lt;/p&gt;

&lt;p&gt;My approach leverages Chromium’s modular architecture while maintaining a clean “downstream” build model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Compatibility:&lt;/strong&gt; Use Chromium’s existing protobuf interfaces for management services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; Rely on a third-party IdP (Identity Provider), as identity management is its own domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-Google:&lt;/strong&gt; Gradually remove Google service dependencies from Chromium components, replacing them with custom implementations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branding:&lt;/strong&gt; Apply custom branding to establish a unique identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build System:&lt;/strong&gt; Instead of a messy fork, I use a &lt;strong&gt;sibling browser layer&lt;/strong&gt; model to manage downstream updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  The “eb” Build Tool
&lt;/h4&gt;

&lt;p&gt;I created the &lt;strong&gt;eb (Enterprise Browser) build tool&lt;/strong&gt;, heavily inspired by &lt;a href="https://brave.com/" rel="noopener noreferrer"&gt;Brave’s&lt;/a&gt; tooling. It pulls a specific Chromium tag and patches the source to include the &lt;code&gt;enterprise_browser&lt;/code&gt; layer as a sibling to the standard &lt;code&gt;/chrome&lt;/code&gt; directory.&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Enterprise Browser Cloud Management (EBCM)
&lt;/h4&gt;

&lt;p&gt;This is where policy rules are defined and assigned to users via &lt;strong&gt;JIT (Just-In-Time) provisioning&lt;/strong&gt;. EBCM integrates with a third-party IdP to provide user authentication and maps policies to specific user roles.&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Enterprise Browser Device Management Server (EBDM Server)
&lt;/h4&gt;

&lt;p&gt;The EBDM acts as the backend authority:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OIDC Authentication:&lt;/strong&gt; The browser presents a signed ID token issued by the IdP to prove identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Fetching:&lt;/strong&gt; The server sends a protobuf payload containing settings (e.g., “Disable Incognito”).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h4&gt;
  
  
  Sign-in &amp;amp; Policy
&lt;/h4&gt;

&lt;p&gt;I adapted Chromium’s OIDC authentication interceptor to authenticate users directly against my EBCM via the IdP. Currently, the system supports global policy settings driven through Chromium’s device management protobuf definitions.&lt;/p&gt;

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/KodeGood/enterprise-browser-chromium" rel="noopener noreferrer"&gt;Enterprise Browser Chromium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/KodeGood/enterprise-browser-cloud" rel="noopener noreferrer"&gt;Enterprise Browser Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The project is currently in a very early prototyping stage.&lt;/p&gt;

&lt;p&gt;This lays the foundation. Deeper dives into enterprise browser architecture will follow.&lt;/p&gt;

</description>
      <category>browser</category>
      <category>enterprisebrowser</category>
      <category>zerotrust</category>
      <category>chromium</category>
    </item>
    <item>
      <title>Ladybird Browser in an Embedded Web Runtime</title>
      <dc:creator>Jani Hautakangas</dc:creator>
      <pubDate>Sat, 29 Nov 2025 10:44:57 +0000</pubDate>
      <link>https://dev.to/janihau/ladybird-browser-in-an-embedded-web-runtime-2pb7</link>
      <guid>https://dev.to/janihau/ladybird-browser-in-an-embedded-web-runtime-2pb7</guid>
      <description>&lt;p&gt;In my previous &lt;a href="https://dev.to/janihau/build-embedded-debian-web-runtime-with-the-power-of-isar-150a"&gt;blog post&lt;/a&gt;, I showed how to build and create an experimental web runtime using &lt;strong&gt;Isar&lt;/strong&gt;, &lt;strong&gt;Debian Bookworm&lt;/strong&gt;, and &lt;strong&gt;WPEWebKit&lt;/strong&gt;. In that post, I also mentioned that I had a Ladybird version incubating. Now it’s time to reveal it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ladybird&lt;/strong&gt; is a completely new browser, built from scratch instead of relying on any existing browser codebases. It has its roots in &lt;strong&gt;SerenityOS&lt;/strong&gt;, but has since diverged into its own project. The project is still in its early phases, so I didn’t expect much to work. To my surprise, it’s already rendering pages quite well. Even &lt;strong&gt;WebGL&lt;/strong&gt; works!&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Ladybird
&lt;/h2&gt;

&lt;p&gt;Ladybird depends on components that aren’t available in Debian Bookworm, such as the &lt;strong&gt;Skia&lt;/strong&gt; and &lt;strong&gt;ANGLE&lt;/strong&gt; graphics libraries. For those, I created dedicated recipes.&lt;/p&gt;

&lt;p&gt;It also requires newer versions of some Debian libraries that aren’t in Bookworm or its backports repository (e.g. &lt;strong&gt;libpng&lt;/strong&gt; with APNG support). For those, I ended up creating a backports repository of my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Debian Backports
&lt;/h2&gt;

&lt;p&gt;I created a new GitHub repository, &lt;a href="https://github.com/KodeGood/debian-backports" rel="noopener noreferrer"&gt;debian-backports&lt;/a&gt;, which includes a &lt;strong&gt;GitHub Actions&lt;/strong&gt; &lt;a href="https://github.com/KodeGood/debian-backports/blob/main/.github/workflows/deb-build-bookworm.yml" rel="noopener noreferrer"&gt;workflow&lt;/a&gt; that downloads package source tarballs from upstream repositories and overlays Debian build rules.&lt;/p&gt;

&lt;p&gt;The workflow uses &lt;strong&gt;QEMU&lt;/strong&gt; to build both amd64 and arm64 versions of the Debian packages. Finally, it publishes the APT repository (created with &lt;strong&gt;reprepro&lt;/strong&gt;) to GitHub Pages.&lt;/p&gt;

&lt;p&gt;The repository is available at: &lt;a href="https://kodegood.github.io/debian-backports" rel="noopener noreferrer"&gt;kodegood.github.io/debian-backports&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Isar/BitBake Recipes
&lt;/h2&gt;

&lt;p&gt;As mentioned, I wrote new recipes for some dependencies like &lt;strong&gt;Skia&lt;/strong&gt; and &lt;strong&gt;ANGLE&lt;/strong&gt;, and then for Ladybird itself.&lt;/p&gt;

&lt;p&gt;I also applied a couple of patches on top of Ladybird. The main one strips out the browser chrome from Ladybird’s UI, since that’s not needed in a web runtime.&lt;/p&gt;

&lt;p&gt;Ladybird uses &lt;strong&gt;Qt&lt;/strong&gt; as its UI library on Linux and Windows. For now, I simply commented out parts of the code, but since Ladybird has a web content view API, a better long-term approach would be to implement a separate lightweight Qt content view.&lt;/p&gt;

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

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

&lt;p&gt;The demo setup is similar to what I had with the WPE version&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debian Bookworm&lt;/strong&gt; as the base platform&lt;/li&gt;
&lt;li&gt;A slightly customized &lt;strong&gt;Weston IVI-shell&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ladybird browser&lt;/strong&gt;, built from source&lt;/li&gt;
&lt;li&gt;Hardware: &lt;strong&gt;Raspberry Pi 5&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Known issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IME integration is not working, meaning no virtual keyboard&lt;/li&gt;
&lt;li&gt;YouTube is not working. For now, Big Buck Bunny plays via a local page and local file using the video tag&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;From the build configuration menu you can select Ladybird to be built.&lt;/p&gt;

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

&lt;p&gt;Finally, after flashing the image to the Raspberry Pi 5:&lt;/p&gt;

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

&lt;p&gt;Note: This is just a tech demo, not a full production web runtime&lt;/p&gt;

&lt;p&gt;If you’d like to try it, the project’s &lt;a href="https://github.com/zhani/webruntime-debian/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; has setup instructions.&lt;/p&gt;

&lt;p&gt;The project is available on GitHub as &lt;a href="https://github.com/zhani/webruntime-debian" rel="noopener noreferrer"&gt;webruntime-debian&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ladybird</category>
      <category>embedded</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Build Embedded Debian Web Runtime with the Power of Isar</title>
      <dc:creator>Jani Hautakangas</dc:creator>
      <pubDate>Sat, 29 Nov 2025 10:29:11 +0000</pubDate>
      <link>https://dev.to/janihau/build-embedded-debian-web-runtime-with-the-power-of-isar-150a</link>
      <guid>https://dev.to/janihau/build-embedded-debian-web-runtime-with-the-power-of-isar-150a</guid>
      <description>&lt;p&gt;When most people think about building something for embedded devices, &lt;strong&gt;Yocto&lt;/strong&gt; is the first tool that comes to mind.&lt;/p&gt;

&lt;p&gt;But there’s another option worth considering, one that blends the stability of &lt;strong&gt;Debian&lt;/strong&gt; with the flexibility of &lt;strong&gt;BitBake&lt;/strong&gt;. In this post, I’ll walk you through how I used &lt;strong&gt;&lt;a href="https://github.com/ilbers/isar" rel="noopener noreferrer"&gt;Isar&lt;/a&gt;&lt;/strong&gt; (Integration System for Automated Root filesystem generation), &lt;strong&gt;Debian Bookworm&lt;/strong&gt;, and &lt;strong&gt;WPEWebKit&lt;/strong&gt; to create an experimental embedded web runtime running on a &lt;strong&gt;Raspberry Pi 5&lt;/strong&gt; and &lt;strong&gt;Intel NUC&lt;/strong&gt;, and why this approach might be worth trying in your next project.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Why Isar?
&lt;/h2&gt;

&lt;p&gt;The build process in Isar will feel familiar if you’ve worked with Yocto: each component is defined in a BitBake recipe, and you can create dependency trees in the same way.&lt;/p&gt;

&lt;p&gt;The biggest difference is that Isar builds everything around Debian tooling and packaging. It fetches packages directly from upstream Debian repositories, giving you stable and well-tested packages out of the box.&lt;/p&gt;

&lt;p&gt;That said, you can still build from source, customize configurations, and add your own packages.&lt;/p&gt;

&lt;p&gt;I’ve been using Isar for some time in a client project to create and maintain Debian platforms for a fleet of industrial PCs. Ever since, I’ve thought it would be a handy tool for creating an embedded web runtime. So, I decided to give it a try and figured others might be interested in the process too.&lt;/p&gt;

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

&lt;p&gt;For this demo, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debian Bookworm&lt;/strong&gt; as the base platform&lt;/li&gt;
&lt;li&gt;A slightly customized &lt;strong&gt;Weston IVI-shell&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WPEWebKit&lt;/strong&gt;, built from source (version 2.48.3)&lt;/li&gt;
&lt;li&gt;Hardware: &lt;strong&gt;Raspberry Pi 5&lt;/strong&gt; and an old &lt;strong&gt;Intel NUC&lt;/strong&gt; (still in its box after many years!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For configuration, I used &lt;strong&gt;kas&lt;/strong&gt; (a tool that makes it easy to set up BitBake-based projects) and &lt;strong&gt;kas-container&lt;/strong&gt; to run the build in a container, avoiding the need to install Isar and its dependencies locally.&lt;/p&gt;

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

&lt;p&gt;I started by creating a configuration structure for my needs, plus BSP layer recipes for the Raspberry Pi 5 and Intel NUC.&lt;/p&gt;

&lt;p&gt;I implemented a few Weston modules to make the system UI look more polished than the default Weston setup.&lt;/p&gt;

&lt;p&gt;Next, I wrote the recipes to build the Weston modules and WPEWebKit.&lt;br&gt;
I wanted Weston to run as a dedicated webruntime user, which required additional configs and PolicyKit rules.&lt;/p&gt;

&lt;p&gt;Getting WPEWebKit up and running took several iterations, a few quirks, and some patching here and there but eventually it worked.&lt;/p&gt;
&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The result was a config screen where you can select the desired build configuration:&lt;/p&gt;

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

&lt;p&gt;Once you hit &lt;strong&gt;Build&lt;/strong&gt;, you’ll see the familiar BitBake process in the terminal:&lt;/p&gt;

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

&lt;p&gt;Finally, after flashing the image to the Raspberry Pi 5:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is just a tech demo, not a full production web runtime. Each application launches a separate WPEWebKit instance, and there’s no runtime manager to coordinate them.&lt;/p&gt;

&lt;p&gt;If you’d like to try it, the project’s &lt;a href="https://github.com/zhani/webruntime-debian/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; has setup instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debian 13&lt;/strong&gt; support is already in progress, and there’s an experimental branch with early &lt;a href="https://github.com/zhani/webruntime-debian/tree/ladybird" rel="noopener noreferrer"&gt;Ladybird browser engine&lt;/a&gt; integration but it’s not finished and currently does not even compile.&lt;/p&gt;

&lt;p&gt;The project is available on GitHub as &lt;a href="https://github.com/zhani/webruntime-debian" rel="noopener noreferrer"&gt;webruntime-debian&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>webdev</category>
      <category>webkit</category>
      <category>raspberrypi</category>
    </item>
  </channel>
</rss>
