<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Dejan Demonjić</title>
    <link>https://demonjic.com/</link>
    <description>Decision records, defect autopsies and field notes from building secure, multi-tenant .NET platforms.</description>
    <atom:link href="https://demonjic.com/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Technical SEO for a Blazor site: what the crawler actually sees</title>
      <link>https://demonjic.com/blog/technical-seo-for-a-blazor-site</link>
      <guid isPermaLink="true">https://demonjic.com/blog/technical-seo-for-a-blazor-site</guid>
      <pubDate>Fri, 16 Oct 2026 11:30:00 GMT</pubDate>
      <description>A crawler sees bytes, not intent. Static server-side rendering, one content source for pages, sitemap and feeds, a real 404 and immutable fingerprinted assets get a Blazor site most of the way. The rest is reading your own output: an early export of this site showed a truncated title, modified dates hours before the published dates, an author link to nowhere and a configuration flag the renderer ignored. Each one with the rule it breaks and the fix.</description>
    </item>
    <item>
      <title>The quality gate that could not see async code</title>
      <link>https://demonjic.com/blog/the-quality-gate-that-could-not-see-async-code</link>
      <guid isPermaLink="true">https://demonjic.com/blog/the-quality-gate-that-could-not-see-async-code</guid>
      <pubDate>Fri, 16 Oct 2026 11:20:00 GMT</pubDate>
      <description>A gate is a measurement, and a measurement has blind spots you must find before you trust it. Our CRAP gate took complexity from source and coverage from a Cobertura report, and the two never met for an async method, because the compiler had moved its body into a state machine. Of 1,946 async methods, two showed any coverage. What that meant for the gate, why the measurement was left unfixed, and the two rules adopted instead.</description>
    </item>
    <item>
      <title>Panel passwords are sealed to the host that uses them</title>
      <link>https://demonjic.com/blog/panel-passwords-sealed-to-the-host</link>
      <guid isPermaLink="true">https://demonjic.com/blog/panel-passwords-sealed-to-the-host</guid>
      <pubDate>Fri, 16 Oct 2026 11:10:00 GMT</pubDate>
      <description>A secret that one process uses should be readable by that process alone. The decision record for the administrator passwords of access-control panels: write-only entry, a fresh AES-GCM key per secret wrapped with RSA-OAEP under the connector host's key, opened in memory only, one escrow export and then a key nobody can export, a TPM tier that excludes escrow, and why machine-scope DPAPI and the web runtime's key ring were the wrong tools.</description>
    </item>
    <item>
      <title>Talking to door controllers: what ISAPI teaches about typed outcomes</title>
      <link>https://demonjic.com/blog/isapi-and-typed-outcomes</link>
      <guid isPermaLink="true">https://demonjic.com/blog/isapi-and-typed-outcomes</guid>
      <pubDate>Fri, 16 Oct 2026 11:00:00 GMT</pubDate>
      <description>A boolean cannot carry what an access-control panel answers. Three measured facts (a failed write with HTTP 200 and no status, an acknowledged write that changed nothing, a reused digest nonce the panel refused) turned a 27-member interface whose every write returned a boolean into a closed set of typed outcomes, a classification order, a read-back rule and a fresh challenge per request; asking which way a door fails added a deadline on anything that unlocks one.</description>
    </item>
    <item>
      <title>SignalR between a cloud and its sites</title>
      <link>https://demonjic.com/blog/signalr-between-a-cloud-and-its-sites</link>
      <guid isPermaLink="true">https://demonjic.com/blog/signalr-between-a-cloud-and-its-sites</guid>
      <pubDate>Fri, 16 Oct 2026 10:40:00 GMT</pubDate>
      <description>A hub that pushes changes from a cloud to on-premises sites is a security boundary and a lifecycle problem: who may connect, what each connection hears, how long an admission holds, and how a client comes back after the server closes it. The design note: a service-client policy, a group per tenant, a fifteen-second sweep with the same check requests get, and a client that starts itself again because automatic reconnect does not follow a server-side Abort.</description>
    </item>
    <item>
      <title>Server-Sent Events in ASP.NET Core 10, and when to pick them over SignalR</title>
      <link>https://demonjic.com/blog/server-sent-events-in-aspnetcore-10</link>
      <guid isPermaLink="true">https://demonjic.com/blog/server-sent-events-in-aspnetcore-10</guid>
      <pubDate>Fri, 16 Oct 2026 10:30:00 GMT</pubDate>
      <description>When a server talks and a client listens for the length of one request, Server-Sent Events is the right transport: plain HTTP, one direction, a text protocol a proxy carries and a browser reconnects by itself. .NET 10 made it a first-class result. The parts that bite are on the client: the protocol has rules, and some servers decide per response whether to answer with JSON or with a stream.</description>
    </item>
    <item>
      <title>One repository, several solutions</title>
      <link>https://demonjic.com/blog/one-repository-several-solutions</link>
      <guid isPermaLink="true">https://demonjic.com/blog/one-repository-several-solutions</guid>
      <pubDate>Fri, 16 Oct 2026 10:20:00 GMT</pubDate>
      <description>Splitting a product line into repositories is a permissions decision wearing an architecture costume. Azure Repos permissions stop at the repository, so if nobody has to be kept from reading part of the code, one repository with several solution files keeps the compile-time safety that packages trade for version drift. The decision record, the reference matrix, and the test that holds it.</description>
    </item>
    <item>
      <title>A schema per tenant: the database owns the name</title>
      <link>https://demonjic.com/blog/a-schema-per-tenant</link>
      <guid isPermaLink="true">https://demonjic.com/blog/a-schema-per-tenant</guid>
      <pubDate>Fri, 16 Oct 2026 10:10:00 GMT</pubDate>
      <description>When every tenant's rows live in a PostgreSQL schema of their own, the mapping from identifier to schema name is a security boundary. So the mapping moved into the database: a stored generated column, a unique index, COLLATE "C", and a plain CREATE SCHEMA as the claim on a new name. With the two error codes that mean 'taken', the one-at-a-time gate, and an EF Core pooling detail that only production-shaped tests found.</description>
    </item>
    <item>
      <title>The tenant comes from the token, not from the header</title>
      <link>https://demonjic.com/blog/bind-the-tenant-to-the-token</link>
      <guid isPermaLink="true">https://demonjic.com/blog/bind-the-tenant-to-the-token</guid>
      <pubDate>Fri, 16 Oct 2026 10:00:00 GMT</pubDate>
      <description>In a multi-tenant API the tenant a request acts in is decided from who is calling. A header may name one; the token says whether the caller may act there. The decision record: one middleware owns the header, the status code follows the caller, and authority that arrives through a replicated directory is trusted for one token lifetime.</description>
    </item>
    <item>
      <title>Mutual TLS and certificate-bound tokens for the links that matter</title>
      <link>https://demonjic.com/blog/mutual-tls-and-certificate-bound-tokens</link>
      <guid isPermaLink="true">https://demonjic.com/blog/mutual-tls-and-certificate-bound-tokens</guid>
      <pubDate>Fri, 16 Oct 2026 09:40:00 GMT</pubDate>
      <description>Client credentials alone let a token travel. For the links between a site and its cloud, and between an assistant's tool servers and the API, every token is bound to the client's certificate (RFC 8705), tokens are exchanged instead of forwarded (RFC 8693), and resources announce their issuer (RFC 9728). The decision record, with the parts that only building it revealed.</description>
    </item>
    <item>
      <title>A Content Security Policy for an identity server</title>
      <link>https://demonjic.com/blog/csp-for-an-identity-server</link>
      <guid isPermaLink="true">https://demonjic.com/blog/csp-for-an-identity-server</guid>
      <pubDate>Fri, 16 Oct 2026 09:30:00 GMT</pubDate>
      <description>The strictest policy a sign-in page can carry, which directive is left out and why, what base-uri 'none' does to a Blazor page, and why the policy is chosen from the endpoint. A design note from building Hexavera's identity host.</description>
    </item>
    <item>
      <title>When the token store is down, the honest answer is 503</title>
      <link>https://demonjic.com/blog/fail-closed-token-checks</link>
      <guid isPermaLink="true">https://demonjic.com/blog/fail-closed-token-checks</guid>
      <pubDate>Fri, 16 Oct 2026 09:20:00 GMT</pubDate>
      <description>A resource server that cannot check a token must refuse it, and a 401 is the wrong refusal: it tells the client its token is bad and signs the user out. The design: fail closed, classify the fault by what the data provider says, answer 503 with a Retry-After drawn per response, and let anonymous routes ask before they say 'not signed in'.</description>
    </item>
    <item>
      <title>Decide from the endpoint, not from the path</title>
      <link>https://demonjic.com/blog/decide-from-the-endpoint</link>
      <guid isPermaLink="true">https://demonjic.com/blog/decide-from-the-endpoint</guid>
      <pubDate>Fri, 16 Oct 2026 09:10:00 GMT</pubDate>
      <description>Rate limits, security headers and access gates are per-endpoint decisions. ASP.NET Core accepts several spellings of one path, so a decision made by comparing the path is a decision made on incomplete information. Put it on the endpoint, and test that every endpoint is covered.</description>
    </item>
    <item>
      <title>One identity server for the cloud and for every site</title>
      <link>https://demonjic.com/blog/identity-server-on-openiddict</link>
      <guid isPermaLink="true">https://demonjic.com/blog/identity-server-on-openiddict</guid>
      <pubDate>Fri, 16 Oct 2026 09:00:00 GMT</pubDate>
      <description>When the same product runs in the cloud and at customer sites, identity is part of the product, so one identity host on OpenIddict and ASP.NET Core Identity serves both. The decision record: the model, the values, the options rejected, and what it costs.</description>
    </item>
    <item>
      <title>Pre-MVP: make it right, never comply</title>
      <link>https://demonjic.com/blog/pre-mvp-make-it-right-never-comply</link>
      <guid isPermaLink="true">https://demonjic.com/blog/pre-mvp-make-it-right-never-comply</guid>
      <pubDate>Sun, 11 Oct 2026 00:00:00 GMT</pubDate>
      <description>Before the first release there is nothing to stay compatible with. Code that adapts to older code, an older schema or stale development data is a defect, not a kindness. The rule, how it was found, where it has held and where it is shaping decisions not yet built, and the day it should end.</description>
    </item>
  </channel>
</rss>