<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Rukia Labs — what we're building</title>
        <link>https://docs.rukialabs.com/sw/blog</link>
        <description>Rukia Labs Docs Blog</description>
        <lastBuildDate>Sat, 05 Sep 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>sw</language>
        <copyright>Copyright © 2026 Rukia Labs</copyright>
        <item>
            <title><![CDATA[One documentation site, two front doors]]></title>
            <link>https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors</link>
            <guid>https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors</guid>
            <pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[These pages are served from two places in two different sets of colours, from a single source.]]></description>
            <content:encoded><![CDATA[<p>This site is served at two addresses:</p>
<div class="language-text codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-text codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><div class="token-line" style="color:#393A34"><span class="token plain">docs.rukialabs.com            Rukia Labs' own colours</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">yourorg.rukialabs.com/docs    your organisation's colours</span><br></div></code></pre></div></div>
<p>Same words, same pages, same build inputs. Only the branding differs — and,
less obviously, the path everything is mounted at.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="why-not-just-one">Why not just one<a href="https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors#why-not-just-one" class="hash-link" aria-label="Direct link to Why not just one" title="Direct link to Why not just one" translate="no">​</a></h2>
<p>An organisation's documentation should look like their product, because inside
their programme it <em>is</em> their product. Sending a learner from a branded console
to a page in somebody else's orange is a seam where there does not need to be
one.</p>
<p>And the general site has to exist too, for everyone who has not signed in to
anything yet.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-colours-are-the-easy-half">The colours are the easy half<a href="https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors#the-colours-are-the-easy-half" class="hash-link" aria-label="Direct link to The colours are the easy half" title="Direct link to The colours are the easy half" translate="no">​</a></h2>
<p>Branding already arrives at the edge, injected into the HTML before the page
runs — see <em>Branding that arrives before the JavaScript does</em>, earlier on this
blog. The docs read the same value and repaint their palette from it.</p>
<p>The one subtlety is ordering. The injected configuration lands at the very end
of <code>&lt;head&gt;</code>, so anything in the head runs too early to see it. The repaint
happens in the first tag of <code>&lt;body&gt;</code> instead: after the whole head, before
anything is painted. Late enough to have the data, early enough that nobody sees
the default first.</p>
<p>A version of this that ran at hydration would also be correct, eventually, and
would flash on every page load — which is the whole thing we were avoiding.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-path-is-the-hard-half">The path is the hard half<a href="https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors#the-path-is-the-hard-half" class="hash-link" aria-label="Direct link to The path is the hard half" title="Direct link to The path is the hard half" translate="no">​</a></h2>
<p>A static site generator compiles its base path into everything: every link,
every asset URL, the client router's notion of where the root is. Ours is no
exception.</p>
<p>That makes one build physically unable to answer at both mounts. Served under
<code>/docs</code>, a build made for <code>/</code> emits a link to <code>/guides/intro</code> — and the first
click walks the reader out of the documentation and into the learner
application, which has never heard of that route.</p>
<p>Rewriting the HTML at the edge does not save it either. You can re-base the
<code>href</code> in the markup; you cannot re-base the base path already compiled into the
router bundle. The first click is client-side and never reaches the edge at all.</p>
<p>So it is two builds of one directory, differing in a single variable. Same
content, same theme, same everything — two artifacts, because the mount point is
a build input rather than a runtime one.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-bit-we-nearly-got-wrong">The bit we nearly got wrong<a href="https://docs.rukialabs.com/sw/blog/one-docs-two-front-doors#the-bit-we-nearly-got-wrong" class="hash-link" aria-label="Direct link to The bit we nearly got wrong" title="Direct link to The bit we nearly got wrong" translate="no">​</a></h2>
<p>Two copies of identical content on different hostnames is a duplicate-content
problem. Left indexable, an organisation's copy competes with the general site
for the same queries, and search engines pick a winner we did not choose.</p>
<p>The per-organisation mounts are <code>noindex</code>. They are for the people already
inside that organisation; the copy meant for everyone else is the one that gets
indexed.</p>
<p>Not a deep insight, but the kind of thing that is free to get right on the day
and expensive to notice six months later.</p>]]></content:encoded>
            <category>Documentation</category>
            <category>Multi-tenancy</category>
            <category>Edge</category>
        </item>
        <item>
            <title><![CDATA[Four frontends, one address]]></title>
            <link>https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment</link>
            <guid>https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment</guid>
            <pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How the learner, instructor and admin consoles share a domain and a session without being one application.]]></description>
            <content:encoded><![CDATA[<p>An organisation's address serves three separate applications:</p>
<div class="language-text codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-text codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><div class="token-line" style="color:#393A34"><span class="token plain">yourorg.rukialabs.com/            learner</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">yourorg.rukialabs.com/instructor  authoring and grading</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">yourorg.rukialabs.com/admin       administration</span><br></div></code></pre></div></div>
<p>They are three builds, from three directories, with three bundles. They share a
domain, a session and a design system, and a person moving between them does not
sign in again.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="why-not-one-application">Why not one application<a href="https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment#why-not-one-application" class="hash-link" aria-label="Direct link to Why not one application" title="Direct link to Why not one application" translate="no">​</a></h2>
<p>The three audiences barely overlap. A learner never opens the grading queue; an
administrator rarely takes a course. Built as one application, everyone
downloads all three — and the admin console, the heaviest of them, is the one
the fewest people open.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="why-not-three-domains">Why not three domains<a href="https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment#why-not-three-domains" class="hash-link" aria-label="Direct link to Why not three domains" title="Direct link to Why not three domains" translate="no">​</a></h2>
<p>Because of the session. Three domains means the sign-in state has to be shared
across origins, and every mechanism for doing that is either a third-party
cookie — increasingly not a mechanism at all — or a redirect dance that is
visible to the user and fragile in exactly the situations where it matters.</p>
<p>One origin means one cookie jar. The session problem disappears rather than
being solved.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-part-that-is-genuinely-awkward">The part that is genuinely awkward<a href="https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment#the-part-that-is-genuinely-awkward" class="hash-link" aria-label="Direct link to The part that is genuinely awkward" title="Direct link to The part that is genuinely awkward" translate="no">​</a></h2>
<p>Serving three applications from one origin means each has to know where it lives.
A build served at <code>/admin</code> has to request its assets from <code>/admin/assets/…</code>,
and its router has to know that <code>/admin</code> is the root and not a route.</p>
<p>That value is compiled in at build time. Which means the same source, built for
a different mount point, is a different artifact — and if the mount point and
the build disagree, the failure is not subtle. You get a blank page and a
console full of 404s for JavaScript files that exist, at a path nobody asked for.</p>
<p>We have hit this from both directions. A build that captured the browser's path
into a post-sign-in redirect — a path that already contained the prefix — and
replayed it through a router that prepends the prefix itself, landing everyone
on <code>/instructor/instructor/</code>. The learner console escaped it because its prefix
is empty, which is exactly why it took a while to see: the bug was invisible in
the application we look at most.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="what-holds-it-together">What holds it together<a href="https://docs.rukialabs.com/sw/blog/four-frontends-one-deployment#what-holds-it-together" class="hash-link" aria-label="Direct link to What holds it together" title="Direct link to What holds it together" translate="no">​</a></h2>
<p>A router at the edge decides which application serves a path, and rewrites the
asset URLs in the HTML on the way back so the browser only ever sees the public
layout. The applications behind it can be one deployment or several, at their
own roots or under their prefixes, and the choice is configuration rather than
code.</p>
<p>That flexibility earns its keep during migrations, and costs something the rest
of the time: there are now two places a mount point is decided, and they have to
agree. Our defence is that disagreement fails loudly — an unconfigured
application returns an error naming the variable to set, rather than quietly
falling back to a default that serves the wrong environment's code under the
right hostname.</p>
<p>That specific failure — "not configured" and "working" being indistinguishable —
is worth more attention than it usually gets. A fallback that makes a broken
configuration look healthy does not prevent the outage. It just moves it to a
worse moment.</p>]]></content:encoded>
            <category>Architecture</category>
            <category>Multi-tenancy</category>
        </item>
        <item>
            <title><![CDATA[Branding that arrives before the JavaScript does]]></title>
            <link>https://docs.rukialabs.com/sw/blog/branding-before-the-bundle</link>
            <guid>https://docs.rukialabs.com/sw/blog/branding-before-the-bundle</guid>
            <pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why every organisation's colours are correct in the first painted frame, and what it took to get there.]]></description>
            <content:encoded><![CDATA[<p>Every organisation on the platform gets its own address and its own branding.
The obvious way to build that is to fetch the branding when the app starts. We
did, and it was wrong in a way that took a while to name.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-flash">The flash<a href="https://docs.rukialabs.com/sw/blog/branding-before-the-bundle#the-flash" class="hash-link" aria-label="Direct link to The flash" title="Direct link to The flash" translate="no">​</a></h2>
<p>Fetching branding on mount produces a sequence: the app boots with defaults,
asks the server who this organisation is, and repaints. On a fast connection
that is a flicker. On a slow one it is two seconds of the wrong logo.</p>
<p>It is worst exactly where it matters most — the sign-in page, the first thing a
new learner sees. They arrive at their own organisation's address and are shown
somebody else's brand while the page makes up its mind.</p>
<p>No amount of optimising the fetch fixes this. The request cannot start until the
bundle runs, and the bundle cannot run until it is downloaded. The branding is
always at least one round trip behind the first paint.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="moving-the-work-in-front-of-the-app">Moving the work in front of the app<a href="https://docs.rukialabs.com/sw/blog/branding-before-the-bundle#moving-the-work-in-front-of-the-app" class="hash-link" aria-label="Direct link to Moving the work in front of the app" title="Direct link to Moving the work in front of the app" translate="no">​</a></h2>
<p>Every request to an organisation's address already passes through our router at
the edge, which is what decides which application serves it. That router knows
the hostname before any application does, and the hostname is precisely what
identifies the organisation.</p>
<p>So it looks the organisation up and writes the result into the HTML on its way
past:</p>
<div class="language-html codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-html codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><div class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script language-javascript dom variable" style="color:#36acaa">window</span><span class="token script language-javascript punctuation" style="color:#393A34">.</span><span class="token script language-javascript property-access">__TENANT__</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator" style="color:#393A34">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#393A34">{</span><span class="token script language-javascript"> </span><span class="token script language-javascript string-property property" style="color:#36acaa">"name"</span><span class="token script language-javascript operator" style="color:#393A34">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#e3116c">"…"</span><span class="token script language-javascript punctuation" style="color:#393A34">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript string-property property" style="color:#36acaa">"logoUrl"</span><span class="token script language-javascript operator" style="color:#393A34">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#e3116c">"…"</span><span class="token script language-javascript punctuation" style="color:#393A34">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript string-property property" style="color:#36acaa">"settings"</span><span class="token script language-javascript operator" style="color:#393A34">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#393A34">{</span><span class="token script language-javascript"> … </span><span class="token script language-javascript punctuation" style="color:#393A34">}</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">head</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></div></code></pre></div></div>
<p>By the time the browser parses the document, the branding is already in it. The
application reads a value that is present before it starts rather than fetching
one, and the first painted frame is correct. There is nothing to flash from.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="what-it-cost">What it cost<a href="https://docs.rukialabs.com/sw/blog/branding-before-the-bundle#what-it-cost" class="hash-link" aria-label="Direct link to What it cost" title="Direct link to What it cost" translate="no">​</a></h2>
<p>Three things, none of them free.</p>
<p><strong>The HTML cannot be shared.</strong> A page carrying one organisation's name must
never be served to another, so these responses are <code>no-store</code>. The HTML shell is
small and the hashed assets behind it still cache normally, so the trade is
mostly fine — but it is a real one, and it is the reason this technique does not
generalise to every page on the internet.</p>
<p><strong>Lookups have to be fast.</strong> A per-request database query in front of every page
load is not viable. The lookup reads a key-value store at the edge, written by
the platform whenever an organisation's settings change, with a short in-process
cache in front of it. Which means writes have to invalidate that cache — a cache
that also caches misses, so a newly-created organisation stays "not found" for
the length of the TTL unless someone purges it. We learned that the direct way.</p>
<p><strong>The fallback has to stay.</strong> Injection can fail: a misconfigured host, a
lookup that times out, local development where there is no edge at all. The
application still fetches its own configuration when the injected value is
absent. The fast path is an optimisation, never a requirement — the moment it
becomes a requirement, every one of those cases becomes an outage.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="where-else-it-goes">Where else it goes<a href="https://docs.rukialabs.com/sw/blog/branding-before-the-bundle#where-else-it-goes" class="hash-link" aria-label="Direct link to Where else it goes" title="Direct link to Where else it goes" translate="no">​</a></h2>
<p>Once the shell is being rewritten at the edge for one reason, other things want
the same treatment. Shared badge and certificate pages now get their
<code>og:</code> tags injected the same way, because link-preview crawlers do not run
JavaScript either — a credential pasted into Slack used to preview as the
generic application shell.</p>
<p>Same mechanism, same constraint: if it has to be in the HTML, it cannot wait for
the bundle.</p>]]></content:encoded>
            <category>Edge</category>
            <category>Multi-tenancy</category>
            <category>Architecture</category>
        </item>
    </channel>
</rss>