<?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>Managed Keycloak Hosting and Enterprise Keycloak Support Blog</title>
        <link>https://phasetwo.io/blog/</link>
        <description>Managed Keycloak Hosting and Enterprise Keycloak Support Blog</description>
        <lastBuildDate>Mon, 06 Jul 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[How We Scaled Keycloak Event Storage with Logs, S3, and ClickHouse]]></title>
            <link>https://phasetwo.io/blog/scaling-keycloak-event-storage/</link>
            <guid>https://phasetwo.io/blog/scaling-keycloak-event-storage/</guid>
            <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How Phase Two moved Keycloak event storage out of the database and into a log-based analytics pipeline — with an open-source EventStoreProvider anyone can use.]]></description>
            <content:encoded><![CDATA[<p>Every login, logout, failed password attempt, and admin change in Keycloak produces an event. That's exactly what you want for security auditing and product analytics — until you realize where Keycloak puts them: <strong>in the same relational database that your authentication path depends on</strong>. At scale, event storage becomes a problem you can't ignore. Here's how we solved it, and how the key piece — an MDC-logging <code>EventStoreProvider</code> — is open source so you can solve it too.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-problem-your-auth-database-is-not-an-analytics-database">The Problem: Your Auth Database Is Not an Analytics Database<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#the-problem-your-auth-database-is-not-an-analytics-database" class="hash-link" aria-label="Direct link to The Problem: Your Auth Database Is Not an Analytics Database" title="Direct link to The Problem: Your Auth Database Is Not an Analytics Database" translate="no">​</a></h2>
<p>Out of the box, Keycloak stores events in the <code>EVENT_ENTITY</code> and <code>ADMIN_EVENT_ENTITY</code> tables via its JPA event store. This works fine for a small realm. It works a lot less fine when you're hosting many busy realms:</p>
<ul>
<li class=""><strong>Event writes ride the request transaction.</strong> The JPA event store persists events inside the same transaction as the request that produced them. Every login is now gated on an extra insert: if the database is slow, the request thread waits; if event persistence fails, it can take the whole request down with it. Your users are paying an event-storage tax on every authentication, and your busiest login day is also your busiest event-write day.</li>
<li class=""><strong>Big tables are operationally fragile tables.</strong> Once <code>EVENT_ENTITY</code> grows into the tens or hundreds of millions of rows, routine maintenance becomes dangerous. A Keycloak upgrade that touches the event schema can hold a lock on a table that every in-flight request is trying to insert into — and now request threads are hanging behind a migration. The same goes for adding an index or any other DDL you'd normally consider harmless.</li>
<li class=""><strong>Expiry is a bulk <code>DELETE</code> against your hot path.</strong> Keycloak's event expiry doesn't make old events quietly disappear — it runs mass deletes against the very tables your logins are writing to, generating lock contention, vacuum/bloat pressure, and I/O spikes. So you're stuck choosing between unbounded growth and periodic self-inflicted incidents, and either way you lose the long history your customers actually want.</li>
<li class=""><strong>Queries that hurt.</strong> "Show me all failed logins for this user over the last 90 days" is an analytics query: a full scan over a huge, write-hot, row-oriented table. Running analytics queries against your production auth database during business hours is how you end up on a status page.</li>
</ul>
<p>Our customers were asking good questions — <em>How many active users did we have last month? When did failed logins spike? Who changed that client configuration?</em> — and the honest answer was that the default event store isn't built to answer them at scale.</p>
<p>We wanted three things at once:</p>
<ol>
<li class="">Keep the auth database small, fast, and boring.</li>
<li class="">Keep events <strong>forever</strong> (or at least long enough for real analytics and audit).</li>
<li class="">Make months of events queryable in milliseconds, with dashboards on top.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-insight-events-are-logs">The Insight: Events Are Logs<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#the-insight-events-are-logs" class="hash-link" aria-label="Direct link to The Insight: Events Are Logs" title="Direct link to The Insight: Events Are Logs" translate="no">​</a></h2>
<p>An authentication event is immutable, timestamped, and append-only. That's not a database row — that's a <strong>log line</strong>. And modern infrastructure is extremely good at moving, storing, and analyzing log lines cheaply.</p>
<p>So instead of treating the database as the system of record for events, we restructured the whole thing as a pipeline:</p>
<!-- -->
<p>Each stage does one job, and each stage is independently scalable. Let's walk through them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-1-an-open-source-eventstoreprovider-that-writes-events-as-structured-logs">Step 1: An Open-Source EventStoreProvider That Writes Events as Structured Logs<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#step-1-an-open-source-eventstoreprovider-that-writes-events-as-structured-logs" class="hash-link" aria-label="Direct link to Step 1: An Open-Source EventStoreProvider That Writes Events as Structured Logs" title="Direct link to Step 1: An Open-Source EventStoreProvider That Writes Events as Structured Logs" translate="no">​</a></h2>
<p>The foundation lives in our open-source <a href="https://github.com/p2-inc/keycloak-events" target="_blank" rel="noopener noreferrer" class="">keycloak-events</a> extension (also on <a href="https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-events" target="_blank" rel="noopener noreferrer" class="">Maven Central</a>). We added a new implementation of Keycloak's <code>EventStoreProvider</code> SPI: the <strong>MDC Logger Event Store</strong> (<code>ext-event-mdc-logger-store</code>).</p>
<p>Instead of (or in addition to) writing events to JPA, it flattens each user and admin event and emits it as a structured log line, with every field carried in the <a href="https://logging.apache.org/log4j/2.x/manual/thread-context.html" target="_blank" rel="noopener noreferrer" class="">MDC</a> (Mapped Diagnostic Context). When Keycloak logs to JSON on stdout — standard practice in containers — every event comes out as a machine-parseable record:</p>
<div class="language-json codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-json codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"timestamp"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"2026-06-22T14:03:11.402Z"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"loggerName"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"io.phasetwo.keycloak.EVENT_LOGGER"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"message"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"Event Logger"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"mdc"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.class"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"USER"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"a1b2c3d4-..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.type"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"LOGIN"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.realmName"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"acme"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.clientId"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"web-app"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.userId"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"8f14e45f-..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.ipAddress"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"203.0.113.7"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.time"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"1750601391402"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"event.detailsJson"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"{\"auth_method\":\"openid-connect\", ...}"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>A few design decisions turned out to matter a lot:</p>
<p><strong>Dual write is a config flag, not a fork.</strong> The provider can wrap Keycloak's built-in JPA event store, so you can run both stores side by side while you build out your pipeline. Events land in the database (so the Admin Console and Events REST API keep working exactly as before) <em>and</em> in your logs. Once your downstream pipeline is trustworthy, flip <code>use-jpa</code> off and your database stops accumulating events entirely:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">export EXT_EVENT_MDC_LOGGER_ENABLED=true</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">kc.sh build</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">kc.sh start \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --spi-events-store-provider=ext-event-mdc-logger-store \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --spi-events-store-ext-event-mdc-logger-store-use-jpa=true</span><br></div></code></pre></div></div>
<p><strong>Events are emitted post-commit.</strong> Events are queued in the Keycloak transaction and only logged after it commits. A rolled-back request never produces a phantom event, so your analytics never disagree with reality.</p>
<p><strong>MDC context is scoped, not leaked.</strong> Keycloak reuses executor threads, and MDC state is thread-local. The provider sets the <code>event.*</code> MDC keys inside a try-with-resources scope and restores the previous values immediately after the log call, so event fields never bleed into unrelated log lines on the same thread.</p>
<p><strong>Separate loggers for user and admin events.</strong> User events go to <code>io.phasetwo.keycloak.EVENT_LOGGER</code> and admin events to <code>io.phasetwo.keycloak.ADMIN_EVENT_LOGGER</code>, which makes downstream routing and filtering trivial — you match on the logger name, not on fragile message parsing.</p>
<p><strong>Graceful degradation.</strong> When JPA is disabled, the provider returns empty query results rather than throwing, so the Admin Console's Events tab degrades quietly instead of erroring.</p>
<p>This provider is <a href="https://github.com/p2-inc/keycloak-events/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" class="">Elastic License 2.0</a> like the rest of the keycloak-events extension — free to use in your own deployments. Everything downstream of it is just standard logging infrastructure, which is exactly the point: <strong>once your events are structured log lines, the entire observability ecosystem becomes your event store.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-2-filter-and-ship--fluent-bit-loki-and-s3">Step 2: Filter and Ship — Fluent Bit, Loki, and S3<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#step-2-filter-and-ship--fluent-bit-loki-and-s3" class="hash-link" aria-label="Direct link to Step 2: Filter and Ship — Fluent Bit, Loki, and S3" title="Direct link to Step 2: Filter and Ship — Fluent Bit, Loki, and S3" translate="no">​</a></h2>
<p>On every node in our clusters, <a href="https://fluentbit.io/" target="_blank" rel="noopener noreferrer" class="">Fluent Bit</a> tails the Keycloak container logs and does the routing:</p>
<ul>
<li class=""><strong>Full Keycloak logs</strong> go to <a href="https://grafana.com/oss/loki/" target="_blank" rel="noopener noreferrer" class="">Loki</a> (itself backed by S3, with a 90-day retention policy) for operational debugging and ad-hoc log queries.</li>
<li class=""><strong>Event lines only</strong> — matched by those two dedicated logger names — get a much more interesting treatment. Fluent Bit strips each record down to just the <code>mdc</code> payload, <strong>redacts PII fields</strong> like IP addresses and usernames before they ever leave the node, and writes the results as JSON-lines files to a dedicated S3 bucket, partitioned by cluster and date.</li>
</ul>
<p>That filtering step is what keeps the pipeline lean. Loki holds everything for the ops team; the events bucket holds exactly one clean, structured record per event and nothing else. S3 becomes the durable, dirt-cheap, infinitely-retained system of record — and because it's just JSON in a bucket, we're never locked into any particular downstream consumer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-3-clickhouse-ingestion-with-s3queue">Step 3: ClickHouse Ingestion with S3Queue<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#step-3-clickhouse-ingestion-with-s3queue" class="hash-link" aria-label="Direct link to Step 3: ClickHouse Ingestion with S3Queue" title="Direct link to Step 3: ClickHouse Ingestion with S3Queue" translate="no">​</a></h2>
<p>For the analytics layer we chose <a href="https://clickhouse.com/" target="_blank" rel="noopener noreferrer" class="">ClickHouse</a>, running alongside each cluster. The ingestion mechanism is one of ClickHouse's best-kept secrets: the <a href="https://clickhouse.com/docs/en/engines/table-engines/integrations/s3queue" target="_blank" rel="noopener noreferrer" class=""><code>S3Queue</code> table engine</a>, which continuously watches an S3 prefix and streams new files in as they appear:</p>
<div class="language-sql codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-sql codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">CREATE</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">TABLE</span><span class="token plain"> keycloak_events</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">s3_keycloak_event_queue </span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">raw String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">ENGINE</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> S3Queue</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">'https://&lt;bucket&gt;.s3.amazonaws.com/*/*/*'</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> JSONAsString</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">SETTINGS </span><span class="token keyword" style="font-style:italic">mode</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">'unordered'</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">         keeper_path </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">'/clickhouse/s3queue/keycloak-events'</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><br></div></code></pre></div></div>
<p>No Kafka, no scheduled batch jobs, no ingestion service to babysit. ClickHouse's Keeper tracks which S3 objects have been processed, giving us reliable exactly-once-style ingestion with zero moving parts beyond ClickHouse itself.</p>
<p>From the queue, a chain of materialized views does the shaping:</p>
<ol>
<li class="">A <strong>raw landing table</strong> (MergeTree, partitioned by month) captures every line with its source file metadata and the extracted cluster, realm, and event class.</li>
<li class=""><strong>Typed tables</strong> for user and admin events extract each <code>event.*</code> field into a proper column — <code>event_type</code>, <code>user_id</code>, <code>client_id</code>, <code>ip_address</code>, <code>operation_type</code>, <code>resource_path</code>, and so on. These use <code>ReplacingMergeTree</code> keyed on ingest time, so if a file is ever re-delivered after a crash, duplicates collapse away at merge time.</li>
<li class=""><strong>Five-minute rollup tables</strong> (<code>SummingMergeTree</code>) pre-aggregate event counts by realm, event type, and error. Five minutes divides evenly into every interval our dashboards offer (15 minutes, 1 hour, 1 day), so a single rollup grain serves every zoom level.</li>
</ol>
<p>One honest lesson from production: we initially experimented with S3-backed MergeTree storage for the ClickHouse tables themselves, and walked it back. Every part write, merge, and TTL operation turned into S3 <code>PutObject</code> churn — on the order of 150 requests per second per cluster — and the API request costs showed up on the bill fast. Local NVMe-backed volumes for the tables, with S3 as the ingestion source and archive, turned out to be the right split.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-4-a-lambda-gateway-to-expose-queries-as-web-services">Step 4: A Lambda Gateway to Expose Queries as Web Services<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#step-4-a-lambda-gateway-to-expose-queries-as-web-services" class="hash-link" aria-label="Direct link to Step 4: A Lambda Gateway to Expose Queries as Web Services" title="Direct link to Step 4: A Lambda Gateway to Expose Queries as Web Services" translate="no">​</a></h2>
<p>ClickHouse lives on a private network, and we didn't want dashboards talking SQL to it directly. So we put a small <strong>AWS Lambda behind an HTTP API Gateway</strong> in front of it, exposing a handful of purpose-built REST endpoints:</p>
<ul>
<li class=""><code>/insights/user-events</code> and <code>/insights/admin-events</code> — filtered, paginated event search (by realm, event type, user, client, IP, time range, plus full-text search over event details)</li>
<li class=""><code>/insights/metrics</code> — named, pre-defined aggregations: successful and failed logins, registrations, password resets, MFA enrollment, lockouts, DAU/WAU/MAU, and a session-concurrency estimate</li>
</ul>
<p>The security model is deliberately narrow. API Gateway validates a <strong>JWT</strong> on every request (issued, naturally, by Keycloak). The Lambda then checks a cluster claim in the token against the cluster being queried, so a tenant can only ever see their own events. And there is no free-form SQL anywhere in the API — every endpoint maps to a parameterized query using ClickHouse's native <code>{param:Type}</code> placeholders, which makes injection structurally impossible rather than merely filtered.</p>
<p>The metrics endpoints read from the rollup tables and re-bucket the five-minute grain to whatever interval the caller asks for, so even "show me a year of logins by day" comes back in tens of milliseconds.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-5-dashboards-people-actually-use">Step 5: Dashboards People Actually Use<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#step-5-dashboards-people-actually-use" class="hash-link" aria-label="Direct link to Step 5: Dashboards People Actually Use" title="Direct link to Step 5: Dashboards People Actually Use" translate="no">​</a></h2>
<p>The final layer is the UI in the Phase Two dashboard: an events section per cluster with two complementary views.</p>
<p><strong>Metrics</strong> shows stacked charts of user events by type and admin events by resource and operation, plus a grid of the questions customers actually ask: successful vs. failed logins over time, daily active users, DAU/WAU/MAU tiles, new registrations, password resets, MFA enrollments and removals, login error breakdowns, and session concurrency. Drag-select on any chart zooms every chart on the page to the same window.</p>
<p><strong>Event search</strong> is a virtualized table over the raw events with the full filter set — realm, event type, user, client, IP, time range, free-text search over event details — and a detail drawer showing the complete event payload with copy-to-clipboard JSON.</p>
<p>The little UX decision we're proudest of is the <strong>drill-down</strong>: click any segment of a stacked bar — say, the <code>LOGIN_ERROR</code> slice at 3 AM on Tuesday — and you land in the event search view, pre-filtered to that event type and that time bucket. Aggregate to instance in one click. That interaction is only cheap because the layers below it are: the chart is a rollup-table query and the drill-down is a typed-table query, both answered by the same gateway in milliseconds.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-we-got">What We Got<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#what-we-got" class="hash-link" aria-label="Direct link to What We Got" title="Direct link to What We Got" translate="no">​</a></h2>
<p>The scoreboard after moving event storage out of the database:</p>
<ul>
<li class=""><strong>The auth database does auth.</strong> Event writes, event expiry deletes, and analytics queries are gone from the hot path entirely.</li>
<li class=""><strong>Retention is no longer a trade-off.</strong> S3 keeps the raw events indefinitely for peanuts; ClickHouse keeps them queryable.</li>
<li class=""><strong>Analytics got fast.</strong> Questions that were previously unaskable — a year of login trends across a busy realm — return interactively.</li>
<li class=""><strong>Every layer is replaceable.</strong> Because the contract between Keycloak and everything else is just structured JSON log lines, you could swap Loki for Elasticsearch, ClickHouse for BigQuery, or our dashboard for Grafana, and the provider wouldn't know or care.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="build-it-yourself">Build It Yourself<a href="https://phasetwo.io/blog/scaling-keycloak-event-storage/#build-it-yourself" class="hash-link" aria-label="Direct link to Build It Yourself" title="Direct link to Build It Yourself" translate="no">​</a></h2>
<p>The piece that makes all of this possible — the MDC Logger <code>EventStoreProvider</code> — is open source in <a href="https://github.com/p2-inc/keycloak-events" target="_blank" rel="noopener noreferrer" class="">p2-inc/keycloak-events</a>, alongside our webhook and scripting event listeners. If you run your own Keycloak, you can drop in the extension, enable the provider, and point whatever log pipeline you already have at those two logger names. The <a href="https://github.com/p2-inc/keycloak-events#mdc-logger-event-store" target="_blank" rel="noopener noreferrer" class="">README</a> covers configuration in detail, and we'd love to hear what you build on top of it.</p>
<p>And if you'd rather not run any of this yourself — the provider, the pipeline, the ClickHouse cluster, the dashboards — that's quite literally what we do. Every Phase Two hosted deployment gets this event analytics stack out of the box, along with a team that has already found the sharp edges for you.</p>
<hr>
<p><strong>Want to talk Keycloak event analytics, or scaling Keycloak in general?</strong></p>
<ul>
<li class="">📩 <a href="mailto:sales@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">Reach out to the team →</a></li>
<li class="">👉 <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Get Started with Free Managed Keycloak</a></li>
</ul>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>events</category>
            <category>open_source</category>
            <category>hosting</category>
            <category>data</category>
        </item>
        <item>
            <title><![CDATA[Introducing the Starter Cluster Tier — Phase Two Keycloak from $149/month]]></title>
            <link>https://phasetwo.io/blog/starter-tier-launch/</link>
            <guid>https://phasetwo.io/blog/starter-tier-launch/</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A new dedicated-cluster tier for teams moving from evaluation to a production-ready Phase Two Keycloak deployment. $149/month, 30-day free trial, all the features of our dedicated clusters. Also covering the wind-down of our free shared-realm offering.]]></description>
            <content:encoded><![CDATA[<p>Today we're launching a new way to run Phase Two: the <strong>Starter</strong> cluster tier, available at <strong>$149 per month</strong>. Starter bundles the same feature set as our existing dedicated clusters into a plan that's accessible for teams getting their use case off the ground or running development environments with lower SLA needs. It also includes a <strong>30-day free trial</strong>, so you can validate your setup before committing.</p>
<p><a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Log into the dashboard</a> and click <strong>Create Cluster</strong> to pick the new tier and provision yours.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-you-get">What you get<a href="https://phasetwo.io/blog/starter-tier-launch/#what-you-get" class="hash-link" aria-label="Direct link to What you get" title="Direct link to What you get" translate="no">​</a></h2>
<p>A Starter cluster is a real dedicated Phase Two Keycloak cluster — not a shared realm carved out of someone else's resources. That means:</p>
<ul>
<li class=""><strong>Your own dedicated compute, network, and storage.</strong> No noisy neighbors, no shared connection limits, no contention for cache.</li>
<li class=""><strong>The full Phase Two enhanced Keycloak distribution</strong> — <a href="https://phasetwo.io/product/organizations/" target="_blank" rel="noopener noreferrer" class="">Organizations</a>, <a href="https://phasetwo.io/product/magic-link/" target="_blank" rel="noopener noreferrer" class="">Magic Links</a>, <a href="https://phasetwo.io/product/webhooks/" target="_blank" rel="noopener noreferrer" class="">Webhooks</a>, the <a href="https://phasetwo.io/product/sso/#idp-wizard" target="_blank" rel="noopener noreferrer" class="">IdP Wizard</a>, and the rest of our <a href="https://github.com/p2-inc" target="_blank" rel="noopener noreferrer" class="">open-source extension library</a> bundled in.</li>
<li class=""><strong>Custom domains</strong> (CNAME) for your login portals.</li>
<li class=""><strong>Cluster observability</strong> — uptime, logs, and usage metrics surfaced through the <a class="" href="https://phasetwo.io/blog/dashboard-launch/">new dashboard</a>.</li>
<li class=""><strong>The same managed CockroachDB tier</strong> powering our larger plans, in partnership with <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">Cockroach Labs</a>.</li>
<li class=""><strong>All the regions we support</strong> — pick whichever is closest to your users.</li>
</ul>
<p>Starter is built to be genuinely capable, with resource limits set where most early-stage teams actually need them — enough headroom for real production workloads and development environments. When you need more, our existing higher-capacity cluster offerings are ready for you. They've proven themselves capable for many teams pushing serious throughput, with a larger resource envelope and a matching SLA — and you can scale straight up from Starter into them without re-provisioning.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="who-its-for">Who it's for<a href="https://phasetwo.io/blog/starter-tier-launch/#who-its-for" class="hash-link" aria-label="Direct link to Who it's for" title="Direct link to Who it's for" translate="no">​</a></h2>
<p>We built Starter for two specific shapes of team:</p>
<ol>
<li class=""><strong>Teams ready to move past evaluation.</strong> You've validated Phase Two on a free realm, you're getting close to a real launch, and you want to put your auth on its own infrastructure with a predictable monthly price.</li>
<li class=""><strong>Dev and staging environments.</strong> You're already running a dedicated cluster in production and want a matching environment that doesn't double your bill. Starter is intentionally priced so a team can comfortably run a cluster-per-environment without thinking twice.</li>
</ol>
<p>If your use case is large or mission-critical from day one, our <a class="" href="https://phasetwo.io/pricing/">higher-capacity clusters</a> remain the right choice. Starter is the on-ramp, not the destination — and clusters scale up from Starter into the larger tiers without a re-provision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="try-it-for-30-days-free">Try it for 30 days, free<a href="https://phasetwo.io/blog/starter-tier-launch/#try-it-for-30-days-free" class="hash-link" aria-label="Direct link to Try it for 30 days, free" title="Direct link to Try it for 30 days, free" translate="no">​</a></h2>
<p>Every new Starter cluster includes a <strong>30-day free trial</strong>. That's enough time to wire your application up against a real cluster, run integration tests, validate SSO connections, and decide whether it fits the way you work. If you're still validating at day 30, talk to us — we'd rather give you the runway than rush you.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-to-provision-one">How to provision one<a href="https://phasetwo.io/blog/starter-tier-launch/#how-to-provision-one" class="hash-link" aria-label="Direct link to How to provision one" title="Direct link to How to provision one" translate="no">​</a></h2>
<ol>
<li class=""><a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Open the dashboard</a>.</li>
<li class="">Click <strong>Create Cluster</strong>.</li>
<li class="">Pick <strong>Starter</strong> as the tier.</li>
<li class="">Choose your region.</li>
<li class="">Watch it provision (most clusters are live within 30 minutes).</li>
</ol>
<p>Detailed setup walkthroughs live in the <a class="" href="https://phasetwo.io/docs/self-service/dedicated-clusters/">Cluster docs</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-this-means-for-our-free-shared-realms">What this means for our free shared realms<a href="https://phasetwo.io/blog/starter-tier-launch/#what-this-means-for-our-free-shared-realms" class="hash-link" aria-label="Direct link to What this means for our free shared realms" title="Direct link to What this means for our free shared realms" translate="no">​</a></h2>
<p>Alongside the Starter launch, <strong>we are discontinuing our free, shared-realm offering</strong>. Free realms have served us well — they've onboarded thousands of teams over the years and given us a clear picture of how people validate Phase Two — but they've also become harder to operate sustainably as our paying-customer footprint has grown.</p>
<p>The Starter tier (with its 30-day trial) is now the path from "I want to try this" to "I'm running it in production." We think it's a clearer journey, and it gives us a single class of infrastructure we can invest in deeply rather than splitting effort between shared and dedicated systems.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="migration-timeline">Migration timeline<a href="https://phasetwo.io/blog/starter-tier-launch/#migration-timeline" class="hash-link" aria-label="Direct link to Migration timeline" title="Direct link to Migration timeline" translate="no">​</a></h3>
<p>If you currently have active realms on our free, shared infrastructure:</p>
<ul>
<li class=""><strong>You have 30 days from today (2026-06-30) — through 2026-07-30 — to upgrade and migrate your realms</strong> if you want to preserve them.</li>
<li class="">After 2026-07-30, the shared infrastructure will be shut down and its data permanently deleted.</li>
</ul>
<p>To migrate, create a Starter (or larger) cluster from the dashboard, export your realm config from the shared instance, and import it into your new cluster. Keycloak's <a href="https://www.keycloak.org/server/importExport" target="_blank" rel="noopener noreferrer" class="">realm import/export guide</a> describes the underlying mechanics, and our team is happy to walk through the migration with you.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="were-here-to-help">We're here to help<a href="https://phasetwo.io/blog/starter-tier-launch/#were-here-to-help" class="hash-link" aria-label="Direct link to We're here to help" title="Direct link to We're here to help" translate="no">​</a></h2>
<ul>
<li class=""><strong>Technical or migration questions</strong> — email <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</li>
<li class=""><strong>Pricing, upgrade paths, or help picking the right tier</strong> — email <a href="mailto:sales@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">sales@phasetwo.io</a>.</li>
</ul>
<p>Whichever way you contact us, the actual humans who built and operate the infrastructure are the ones writing back.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="thank-you">Thank you<a href="https://phasetwo.io/blog/starter-tier-launch/#thank-you" class="hash-link" aria-label="Direct link to Thank you" title="Direct link to Thank you" translate="no">​</a></h2>
<p>If you've used Phase Two — free shared realm or dedicated cluster — you're part of the reason we're able to launch this tier today. We're grateful you picked us for your Keycloak hosting, and we're going to keep working to make the platform more reliable, more affordable, and more useful for what you're building.</p>
<p>If we've helped, we'd love a <a href="https://github.com/p2-inc" target="_blank" rel="noopener noreferrer" class="">⭐ on GitHub</a> or a follow on <a href="https://www.linkedin.com/company/phase-two-tech" target="_blank" rel="noopener noreferrer" class="">LinkedIn</a>. It genuinely matters.</p>
<p>— The Phase Two Team</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>release</category>
            <category>starter</category>
            <category>pricing</category>
            <category>keycloak</category>
            <category>phase_two</category>
            <category>dedicated</category>
        </item>
        <item>
            <title><![CDATA[Observability for Keycloak, with Zero Setup]]></title>
            <link>https://phasetwo.io/blog/observability-launch/</link>
            <guid>https://phasetwo.io/blog/observability-launch/</guid>
            <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two Observability is here. View requests, event data, and live logs for your dedicated Keycloak cluster with zero setup — built right into the dashboard.]]></description>
            <content:encoded><![CDATA[<p>Today we're launching <strong>Observability</strong> for dedicated Keycloak clusters — built directly into the Phase Two dashboard with <strong>zero setup</strong>. Requests, event data, and live logs are all there the moment your cluster is running. No agents to install, no log shippers to configure, no Prometheus, Grafana, or Loki stack to stand up and maintain.</p>
<p>This is the feature we're most excited about this year, because of what it changes about getting Keycloak into production. Standing up the observability layer around a Keycloak cluster — metrics pipelines, log aggregation, dashboards, alerting — is the kind of work that normally takes a devops team <strong>weeks to months</strong>. With Phase Two, it's already done. Getting up and running on a fully observable Keycloak cluster is now a matter of <strong>minutes</strong>.</p>
<div class="theme-admonition theme-admonition-info admonition_Y66Y alert alert--info"><div class="admonitionHeading_Feou"><span class="admonitionIcon_RBEH"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_oUgd"><p><strong>Available for Dedicated Clusters</strong>
Observability is included with all Phase Two <strong>dedicated cluster</strong> plans. Open the <strong>Metrics</strong> or <strong>Logs</strong> section on your cluster in the <a href="https://dash.phasetwo.io/clusters" target="_blank" rel="noopener noreferrer" class="">self-service dashboard</a> to get started.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="see-your-whole-system-at-a-glance">See your whole system at a glance<a href="https://phasetwo.io/blog/observability-launch/#see-your-whole-system-at-a-glance" class="hash-link" aria-label="Direct link to See your whole system at a glance" title="Direct link to See your whole system at a glance" translate="no">​</a></h2>
<p>Start with the big picture: <em>understanding what your cluster is doing</em>, at a glance, without building a single chart yourself.</p>
<p><strong>Event Metrics</strong> chart Keycloak authentication and admin activity over any time range: user events by type, successful and failed logins, active users per day, new registrations, password resets, MFA enrollment, and login-error reasons.</p>
<figure><img src="https://phasetwo.io/docs/observability/user-events.png" alt="Event Metrics — user events by type charted over time"><figcaption>Event Metrics: authentication and admin activity, derived from Keycloak events.</figcaption></figure>
<p><strong>Request Metrics</strong> chart the HTTP traffic and performance of the cluster: requests by endpoint type and status class, error rate, cache hit ratio, latency and TTFB percentiles (p50 / p95 / p99), bytes served, top endpoints, and top user agents.</p>
<figure><img src="https://phasetwo.io/docs/observability/requests.png" alt="Request Metrics — requests by endpoint type charted over time"><figcaption>Request Metrics: HTTP traffic, errors, caching, and latency for the cluster.</figcaption></figure>
<p>Spot an anomaly in a chart, then drop straight into the logs to find the exact requests and events behind it. The metrics tell you <em>something changed</em>; the logs tell you <em>exactly what</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="debugging-that-takes-minutes-not-days">Debugging that takes minutes, not days<a href="https://phasetwo.io/blog/observability-launch/#debugging-that-takes-minutes-not-days" class="hash-link" aria-label="Direct link to Debugging that takes minutes, not days" title="Direct link to Debugging that takes minutes, not days" translate="no">​</a></h2>
<p>When something goes wrong with an authentication flow — a login that fails intermittently, a theme that won't load, a token error that only some users hit — the slowest part has always been <em>seeing what actually happened</em>. Traditionally that meant SSH-ing into nodes, grepping through container logs, or waiting for someone with cluster access to pull the right files.</p>
<p>Phase Two Observability collapses that loop. Open the <strong>Logs</strong> section, filter to the realm and time window, and watch the relevant lines stream in live.</p>
<figure><img src="https://phasetwo.io/docs/dashboard/cluster-observability-stream-logs-2.png" alt="Phase Two Dash — live Cluster Logs streaming with filters for time range, realm, level, and search"><figcaption>Stream and inspect cluster logs live — filter by time range, realm, and level, then click any line for the full structured payload.</figcaption></figure>
<p>The streaming view gives you everything you need to chase down an issue without leaving the browser:</p>
<ul>
<li class=""><strong>Live streaming</strong> with auto-refresh, so new lines appear as they happen</li>
<li class=""><strong>Realm and level filters</strong> (<code>INFO</code>, <code>WARN</code>, <code>ERROR</code>, <code>DEBUG</code>, <code>TRACE</code>, <code>FATAL</code>)</li>
<li class=""><strong>LogQL-style search</strong> — <code>|=</code> to match, <code>!=</code> to exclude, <code>|~</code> for regex. For example, <code>|= "LOGIN_ERROR" != "admin-cli"</code> finds login failures while filtering out a known source, and <code>|= "NullPointerException"</code> surfaces Java exception lines instantly.</li>
<li class=""><strong>Click into any line</strong> to open the full payload, logger name, and stack trace</li>
</ul>
<p>What used to be a multi-hour, multi-person investigation becomes a few clicks.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-included">What's included<a href="https://phasetwo.io/blog/observability-launch/#whats-included" class="hash-link" aria-label="Direct link to What's included" title="Direct link to What's included" translate="no">​</a></h2>
<p>Observability is split into two sections in the dashboard, both fully time-range and realm filterable, with all timestamps in <strong>UTC</strong>:</p>
<ul>
<li class=""><strong>Metrics</strong> — <a href="https://phasetwo.io/docs/self-service/metrics/#event-metrics" target="_blank" rel="noopener noreferrer" class="">Event Metrics</a> and <a href="https://phasetwo.io/docs/self-service/metrics/#request-metrics" target="_blank" rel="noopener noreferrer" class="">Request Metrics</a>: aggregated, charted views of authentication activity and HTTP traffic.</li>
<li class=""><strong>Logs</strong> — <a href="https://phasetwo.io/docs/self-service/logs/#cluster-logs" target="_blank" rel="noopener noreferrer" class="">Cluster Logs</a> (live streaming), <a href="https://phasetwo.io/docs/self-service/logs/#event-logs" target="_blank" rel="noopener noreferrer" class="">Event Logs</a>, <a href="https://phasetwo.io/docs/self-service/logs/#request-logs" target="_blank" rel="noopener noreferrer" class="">Request Logs</a>, and <a href="https://phasetwo.io/docs/self-service/logs/#download-logs" target="_blank" rel="noopener noreferrer" class="">Download Logs</a> for retained files you can pull for offline review, auditing, or support cases.</li>
</ul>
<p>This is the full realization of the observability work we <a href="https://phasetwo.io/blog/cluster-observability-and-logs/" target="_blank" rel="noopener noreferrer" class="">previewed back in February</a>, which started with log downloads. Live streaming, structured event and request views, and the metrics charts are all here now.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-this-matters">Why this matters<a href="https://phasetwo.io/blog/observability-launch/#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters" translate="no">​</a></h2>
<p>Running Keycloak yourself means running everything around it too — and the observability stack is one of the heaviest parts of that "everything." Phase Two gives you that layer for free, built in, from the moment your cluster comes up. Your team spends its time shipping authentication features and resolving issues, not building and babysitting monitoring infrastructure.</p>
<p>Zero setup. Requests, events, and logs in one place. Debugging in minutes. A production-ready, observable Keycloak cluster in the time it takes to read this post.</p>
<hr>
<p>Ready to try it? Log in to the <a href="https://dash.phasetwo.io/clusters" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and open the <strong>Metrics</strong> and <strong>Logs</strong> sections on your cluster. Learn more in the <a href="https://phasetwo.io/docs/self-service/observability/" target="_blank" rel="noopener noreferrer" class="">Observability documentation</a>. Questions? Reach us at <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>self-service</category>
            <category>observability</category>
            <category>metrics</category>
            <category>logs</category>
            <category>keycloak</category>
            <category>dedicated-clusters</category>
        </item>
        <item>
            <title><![CDATA[Migrating from WorkOS to Keycloak: A Practical Walkthrough]]></title>
            <link>https://phasetwo.io/blog/workos-keycloak-migration/</link>
            <guid>https://phasetwo.io/blog/workos-keycloak-migration/</guid>
            <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An open-source tool that imports users, organizations, roles, SSO connections, and SCIM directories from WorkOS into Phase Two-enabled Keycloak — plus the why, the how, and what to do once it's done.]]></description>
            <content:encoded><![CDATA[<p>A few quarters ago you got handed a single-line ask: <strong>"we need enterprise SSO and directory sync by the end of the quarter."</strong> Maybe the deal was a Fortune-500 logo. Maybe it was a Series B requirement. Either way you found WorkOS, wired in their SDK in a long weekend, shipped the deal, and got the high five.</p>
<p>Then the renewal came in. The seat-based pricing, that sounded harmless when you had two customers using SSO, looks different when you have forty. Suddenly there's a line item on a board slide that scales linearly with your enterprise revenue — a parasite that eats into the very margin that the enterprise tier was supposed to fund. The CFO walks over and asks you to "fix it."</p>
<p>Here is the awkward truth nobody tells the engineer-on-the-spot: <strong>the WorkOS feature set has had a fully open-source equivalent for years</strong>. Keycloak handles SSO. Phase Two's <a href="https://phasetwo.io/product/organizations/" target="_blank" rel="noopener noreferrer" class="">organizations</a> extension handles multi-tenant orgs. The <a href="https://phasetwo.io/product/sso/#idp-wizard" target="_blank" rel="noopener noreferrer" class="">identity provider wizard</a> handles the same admin-portal flow your customers see in WorkOS today. The catch is that nobody wanted to spend the runway to migrate.</p>
<p>We've now built the tool that turns that "we'll deal with it later" debt into an afternoon of work. Why? Because WorkOS customers are starting to wake up to Keycloak, and they're coming to us in droves.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-we-built">What we built<a href="https://phasetwo.io/blog/workos-keycloak-migration/#what-we-built" class="hash-link" aria-label="Direct link to What we built" title="Direct link to What we built" translate="no">​</a></h2>
<p>The <a href="https://github.com/p2-inc/workos-keycloak-migrator" target="_blank" rel="noopener noreferrer" class=""><code>workos-keycloak-migrator</code></a> is a small multi-module Maven project that does three things in lockstep:</p>
<!-- -->
<table><thead><tr><th>Component</th><th>What it does</th></tr></thead><tbody><tr><td><strong>Bulk migrator CLI</strong></td><td>A standalone <code>java -jar</code> that reads everything WorkOS exposes via API and writes it into Keycloak + Phase Two. Idempotent — re-run it whenever you want a fresh reconciliation.</td></tr><tr><td><strong>Webhook listener extension</strong></td><td>A Keycloak <code>RealmResourceProvider</code> that subscribes to WorkOS webhooks and applies live updates while the two systems run in parallel.</td></tr><tr><td><strong>Slow-migration extension</strong></td><td>Bridges <a href="https://github.com/daniel-frak/keycloak-user-migration" target="_blank" rel="noopener noreferrer" class="">keycloak-user-migration</a> to WorkOS so users who haven't been touched in a while can still log in with their WorkOS password the first time and get materialised into Keycloak on demand.</td></tr></tbody></table>
<p>Every entity we touch carries a <code>workos.id</code> attribute so the migrator can recognise it on every subsequent run — no duplicates, no drift.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-to-use-it">How to use it<a href="https://phasetwo.io/blog/workos-keycloak-migration/#how-to-use-it" class="hash-link" aria-label="Direct link to How to use it" title="Direct link to How to use it" translate="no">​</a></h2>
<p>We deliberately kept the runbook short. Here are the six steps:</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="1-build-the-artifacts">1. Build the artifacts<a href="https://phasetwo.io/blog/workos-keycloak-migration/#1-build-the-artifacts" class="hash-link" aria-label="Direct link to 1. Build the artifacts" title="Direct link to 1. Build the artifacts" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mvn -DskipTests package</span><br></div></code></pre></div></div>
<p>This produces three jars:</p>
<ul>
<li class=""><code>migrator/target/workos-keycloak-migrator.jar</code> — the bulk runner.</li>
<li class=""><code>extensions/webhook-listener/target/workos-webhook-listener.jar</code> — drop into Keycloak's <code>providers/</code> directory.</li>
<li class=""><code>extensions/slow-migration/target/workos-slow-migration.jar</code> — same place.</li>
</ul>
<p>The build is plain Maven so it slots into whatever CI you already use.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="2-stand-up-a-phase-two-enabled-keycloak">2. Stand up a Phase Two-enabled Keycloak<a href="https://phasetwo.io/blog/workos-keycloak-migration/#2-stand-up-a-phase-two-enabled-keycloak" class="hash-link" aria-label="Direct link to 2. Stand up a Phase Two-enabled Keycloak" title="Direct link to 2. Stand up a Phase Two-enabled Keycloak" translate="no">​</a></h3>
<p>If you already run Phase Two's <a href="https://phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">hosted offering</a> or your own Phase Two containers, point at that. If you're trying this locally first, the repo ships a <code>docker-compose.yml</code> that brings up Postgres plus the <a href="https://quay.io/repository/phasetwo/phasetwo-keycloak" target="_blank" rel="noopener noreferrer" class="">phasetwo-keycloak image</a> with both extension jars mounted:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">docker compose up -d</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="3-bootstrap-the-realm">3. Bootstrap the realm<a href="https://phasetwo.io/blog/workos-keycloak-migration/#3-bootstrap-the-realm" class="hash-link" aria-label="Direct link to 3. Bootstrap the realm" title="Direct link to 3. Bootstrap the realm" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">./scripts/bootstrap-realm.sh</span><br></div></code></pre></div></div>
<p>This creates the <code>migrate-target</code> realm, flips a couple of Keycloak defaults that would otherwise eat our attributes (<code>unmanagedAttributePolicy=ENABLED</code> and <code>sslRequired=NONE</code> for local HTTP), and creates a <code>migrator-cli</code> service-account client with <code>realm-admin</code>. The script prints the client secret you need for step 4.</p>
<p><strong>Why a service-account client?</strong> The bulk migrator authenticates against Keycloak using OAuth client-credentials. Putting it in its own client (instead of, say, reusing the <code>admin</code> master user) keeps the credential surface area small and auditable.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="4-run-the-bulk-migrator">4. Run the bulk migrator<a href="https://phasetwo.io/blog/workos-keycloak-migration/#4-run-the-bulk-migrator" class="hash-link" aria-label="Direct link to 4. Run the bulk migrator" title="Direct link to 4. Run the bulk migrator" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">java -jar migrator/target/workos-keycloak-migrator.jar \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --workos-api-key=$WORKOS_API_KEY \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --keycloak-url=https://your-keycloak.example.com \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --keycloak-realm=migrate-target \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --keycloak-client-id=migrator-cli \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --keycloak-client-secret=$KC_CLIENT_SECRET \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  --source-label=production</span><br></div></code></pre></div></div>
<p>The runner walks WorkOS in dependency order — environment roles first, then organizations, then per-org roles, then identity-provider stubs, then SCIM stubs, then users, then memberships, then directory users — and prints a summary at the end:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">============= Migration summary =============</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">role:                    {CREATED=5}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">organization:            {CREATED=7}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">organization_role:       {CREATED=38}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">identity_provider:       {PARTIAL=2}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">user:                    {CREATED=10}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">organization_membership: {CREATED=10}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Total failed: 0</span><br></div></code></pre></div></div>
<p>Re-running picks up where the last run left off (cursors are persisted on the realm), and reports <code>SKIPPED reason=unchanged</code> for anything that hasn't drifted since the last sync. This is what lets you keep both systems in parallel — run the migrator daily in a cron job and the WorkOS state shows up in Keycloak each morning.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="5-optional-enable-the-extensions">5. (Optional) Enable the extensions<a href="https://phasetwo.io/blog/workos-keycloak-migration/#5-optional-enable-the-extensions" class="hash-link" aria-label="Direct link to 5. (Optional) Enable the extensions" title="Direct link to 5. (Optional) Enable the extensions" translate="no">​</a></h3>
<p>Both Keycloak extensions are <strong>opt-in per realm</strong> because we don't want a one-shot <code>docker compose up</code> to fan out across every realm in a shared cluster. Set:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">KC_SPI_REALM_RESTAPI_EXTENSION_WORKOS_WEBHOOK_REALMS=migrate-target</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">KC_SPI_REALM_RESTAPI_EXTENSION_WORKOS_LEGACY_REALMS=migrate-target</span><br></div></code></pre></div></div>
<p>(or <code>*</code> for "every realm") and restart Keycloak. The webhook listener will auto-register a WorkOS webhook endpoint on first boot; the slow-migration extension will install the federation component that asks our resource for users it doesn't recognise.</p>
<p>The webhook listener requires an HTTPS URL because WorkOS won't accept anything else for delivery. If you only have HTTP locally, the listener still serves traffic — it just skips the auto-provisioning step and waits for you to register an HTTPS endpoint yourself.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="6-re-run-for-reconciliation-whenever-you-like">6. Re-run for reconciliation whenever you like<a href="https://phasetwo.io/blog/workos-keycloak-migration/#6-re-run-for-reconciliation-whenever-you-like" class="hash-link" aria-label="Direct link to 6. Re-run for reconciliation whenever you like" title="Direct link to 6. Re-run for reconciliation whenever you like" translate="no">​</a></h3>
<p>The bulk migrator is idempotent and cheap to re-run. Many teams run it nightly during the parallel-operation window, then switch off WorkOS once they're happy with the diff.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-the-migrator-cannot-import-and-why">What the migrator cannot import (and why)<a href="https://phasetwo.io/blog/workos-keycloak-migration/#what-the-migrator-cannot-import-and-why" class="hash-link" aria-label="Direct link to What the migrator cannot import (and why)" title="Direct link to What the migrator cannot import (and why)" translate="no">​</a></h2>
<p>There are two pieces of WorkOS state that we cannot pull through the API:</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="sso-connection-secrets-and-metadata">SSO connection secrets and metadata.<a href="https://phasetwo.io/blog/workos-keycloak-migration/#sso-connection-secrets-and-metadata" class="hash-link" aria-label="Direct link to SSO connection secrets and metadata." title="Direct link to SSO connection secrets and metadata." translate="no">​</a></h3>
<p>The WorkOS <code>/connections</code> endpoint exposes the connection's <em>type</em> (Okta SAML, Azure SAML, Google OIDC, etc.) and the organization it's attached to, but <strong>not</strong> the SAML metadata URL, the OIDC client_id/secret, or any of the SSO/SLO URLs. Those were entered into WorkOS through their admin portal by the customer's IT team and are not retrievable.</p>
<p>The migrator handles this honestly: it creates a Keycloak identity provider for every connection with the right <code>providerId</code> (<code>saml</code>, <code>oidc</code>, <code>google</code>, <code>microsoft</code>, etc.), copies the SAML signing certificate when WorkOS happens to expose it, and tags every record with <code>workos.incomplete=true</code> and <code>workos.connection_id=&lt;the original id&gt;</code>. You end up with a placeholder you can finish in two clicks via Phase Two's <a href="https://phasetwo.io/extensions/idp-wizard/" target="_blank" rel="noopener noreferrer" class="">IdP Wizard</a> — or even better, by sending a <a href="https://phasetwo.io/extensions/admin-portal/" target="_blank" rel="noopener noreferrer" class="">portal link</a> to the customer's IT contact and letting them re-walk the same setup flow they already know.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="scim-directory-connection-auth">SCIM directory connection auth<a href="https://phasetwo.io/blog/workos-keycloak-migration/#scim-directory-connection-auth" class="hash-link" aria-label="Direct link to SCIM directory connection auth" title="Direct link to SCIM directory connection auth" translate="no">​</a></h3>
<p>Same shape: <code>/directories</code> tells you the directory exists, what provider (Okta SCIM, generic SCIM 2.0, Azure SCIM, etc.) and which organization it belongs to — but the bearer token your customer's IdP uses to push users is opaque to the WorkOS API. The migrator creates a Phase Two SCIM provider on the right org with a placeholder secret and the same <code>workos.directory.incomplete=true</code> tag so it stands out in the admin UI.</p>
<p>In both cases the customer's IT team has to re-establish the secret. The good news: they only have to do it once, and Phase Two's admin portal flow is the same one they already used for WorkOS, just with a different brand on it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-happens-after-the-migrator-runs">What happens after the migrator runs<a href="https://phasetwo.io/blog/workos-keycloak-migration/#what-happens-after-the-migrator-runs" class="hash-link" aria-label="Direct link to What happens after the migrator runs" title="Direct link to What happens after the migrator runs" translate="no">​</a></h2>
<p>Three things you'll want to do in the days following the migration:</p>
<p><strong>1. Walk the <code>workos.incomplete=true</code> records.</strong> Filter your Phase Two organizations by that attribute and you'll see exactly which connections need attention. Send a portal link to each affected customer's IT contact. They click through the same identity-provider wizard their team used in WorkOS; ten minutes later the IdP is live in Keycloak and the <code>workos.incomplete</code> tag goes away on the next reconciliation run.</p>
<p><strong>2. Audit the <code>scim-managed</code> realm role.</strong> Every user that originated from a WorkOS Directory Sync gets the <code>scim-managed</code> realm role plus <code>scim.directory_id</code> / <code>scim.directory_user_id</code> attributes. That's your "do not deprovision" list — anyone with that role is being managed by upstream SCIM, so a manual admin deletion is going to get reversed. The role gives you an easy filter in the admin UI and a stable bit of state you can hook into your own provisioning logic.</p>
<p><strong>3. Install the slow-migration extension (if you haven't already) before pointing your login UI at Keycloak.</strong> Without it, users whose passwords never made it through the migration (WorkOS doesn't let us export them — they're hashed, scoped to WorkOS's authentication endpoint) will fail their first login. With it, their first login gets verified against WorkOS by our extension, then their password hash is captured and stored in Keycloak. After that they look identical to a native Keycloak user.</p>
<p>Once those three things are done you can flip your AuthKit-style flows to Keycloak, point your application at the new realm, and start the WorkOS cancellation timer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-bigger-picture">The bigger picture<a href="https://phasetwo.io/blog/workos-keycloak-migration/#the-bigger-picture" class="hash-link" aria-label="Direct link to The bigger picture" title="Direct link to The bigger picture" translate="no">​</a></h2>
<p>If you've made it this far you're probably nodding along — the technical migration is straightforward, the gotchas are small, and the math on the new cost line is much better. But it's worth saying out loud what the rest of the move looks like.</p>
<p>WorkOS's pitch was always "we make enterprise auth easy for SaaS developers." The pitch worked because, until recently, the open-source alternative had no admin portal, no IdP wizard, no SCIM Box, no domain verification flow — only the raw Keycloak primitives. To replicate WorkOS's developer experience you had to build the whole admin surface yourself.</p>
<p><a href="https://phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two has built that admin surface.</a> The <a href="https://phasetwo.io/extensions/organizations/" target="_blank" rel="noopener noreferrer" class="">organizations</a> extension gives you multi-tenant orgs with role-based assignments. The <a href="https://phasetwo.io/extensions//idp-wizard/" target="_blank" rel="noopener noreferrer" class="">IdP Wizard</a> replaces the WorkOS connection setup flow. Phase Two SCIM gives you the same managed directory-sync experience. The <a href="https://phasetwo.io/extensions/admin-portal" target="_blank" rel="noopener noreferrer" class="">portal link</a> experience lets you delegate SSO/SCIM setup to your customer's IT team without giving them admin access to your Keycloak — exactly the same delegation model WorkOS uses, with the same UX, just running on Keycloak you control.</p>
<p>The crucial difference, and the reason the migration is worth doing: <a href="https://phasetwo.io/pricing/hosting/" target="_blank" rel="noopener noreferrer" class=""><strong>Phase Two's pricing doesn't grow with your enterprise revenue.</strong></a> SSO, SCIM, organizations, branding, the wizard — they're all part of the platform, not metered features. Your cost stays a predictable budget line item whether you have ten enterprise customers or ten thousand.</p>
<p>The migrator we just walked through is, in that sense, the smallest part of the story. It's the bridge that lets a team that originally chose WorkOS for time-to-market cross over to a platform that doesn't tax that decision forever.</p>
<p>If you've got a WorkOS migration on your roadmap and would like a hand, <a href="https://phasetwo.io/contact/" target="_blank" rel="noopener noreferrer" class="">reach out</a> — we've now done this end-to-end on a real WorkOS Sandbox tenant and the tooling is open source. We'd be happy to walk you through it.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>workos</category>
            <category>migration</category>
            <category>sso</category>
            <category>organizations</category>
            <category>scim</category>
            <category>open_source</category>
        </item>
        <item>
            <title><![CDATA[Experimental SCIM 2.0 provisioning for Organizations]]></title>
            <link>https://phasetwo.io/blog/orgs-scim-experimental/</link>
            <guid>https://phasetwo.io/blog/orgs-scim-experimental/</guid>
            <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Per-organization SCIM 2.0 endpoints for Keycloak, built on Metatavu's keycloak-scim-server. Experimental, but already in production use.]]></description>
            <content:encoded><![CDATA[<p>We're shipping experimental SCIM 2.0 provisioning for the Phase Two <a href="https://github.com/p2-inc/keycloak-orgs" target="_blank" rel="noopener noreferrer" class="">Organizations extension</a>. Each organization in a realm can now act as its own SCIM 2.0 service provider, so an upstream IdP like Okta or Entra ID can push users into a specific tenant rather than into the realm as a whole.</p>
<p>This is the piece of the multi-tenant story that Keycloak's stock SCIM support doesn't address today, and it's been a heavily requested item from customers running Organizations in production.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-this-differs-from-keycloaks-experimental-scim">How this differs from Keycloak's experimental SCIM<a href="https://phasetwo.io/blog/orgs-scim-experimental/#how-this-differs-from-keycloaks-experimental-scim" class="hash-link" aria-label="Direct link to How this differs from Keycloak's experimental SCIM" title="Direct link to How this differs from Keycloak's experimental SCIM" translate="no">​</a></h2>
<p>Keycloak shipped its own experimental SCIM 2.0 capability in version <code>26.6</code>, and it's worth being clear about how the two relate, because the names overlap but the scopes don't:</p>
<ul>
<li class=""><strong>Keycloak's SCIM is realm-level.</strong> An external IdP provisions users into the realm. There is one SCIM endpoint per realm and one population of users. That model is fine when a realm represents a single customer or a single workforce — it's the classic Keycloak deployment shape.</li>
<li class=""><strong>Our SCIM is organization-level.</strong> An external IdP provisions users into a specific organization within a realm. There is one SCIM endpoint <em>per organization</em>, and each one has its own authentication configuration, its own optional IdP federation link, and its own member graph. The realm can host thousands of organizations, each backed by a different upstream identity system, without those populations bleeding into each other.</li>
</ul>
<p>If you're running a B2B SaaS where each customer is an Organization, the realm-level model isn't what you want — you'd be asking every customer's IT department to provision into a shared pool. The per-organization model is the natural fit: each customer's IdP gets its own SCIM URL, its own credentials, and provisions only into its own tenant.</p>
<p>Both are experimental. They're not in competition — they're solving different problems at different layers — and over time we expect the realm-level and organization-level capabilities to coexist in the same Keycloak deployment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-in-the-release">What's in the release<a href="https://phasetwo.io/blog/orgs-scim-experimental/#whats-in-the-release" class="hash-link" aria-label="Direct link to What's in the release" title="Direct link to What's in the release" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="Organization SCIM Admin UI" src="https://phasetwo.io/assets/images/2026-05-27-orgs-scim-ui-4e424e712500eada55888947fdbe413f.png" width="2996" height="1694" class="img_R7yg"></p>
<p>The capability ships disabled by default and is gated by a realm-level flag. Once enabled, each organization gets:</p>
<ul>
<li class="">A SCIM 2.0 endpoint at <code>/realms/{realm}/scim/v2/organizations/{orgId}/</code> exposing the standard SCIM resources (<code>/ServiceProviderConfig</code>, <code>/ResourceTypes</code>, <code>/Schemas</code>, <code>/Users</code>, <code>/Groups</code>).</li>
<li class="">A <strong>SCIM</strong> tab in the organization detail page of the admin UI for configuring authentication and provisioning behavior.</li>
<li class="">A REST configuration API at <code>/realms/{realm}/orgs/{orgId}/scim</code> for the same operations.</li>
<li class="">Four authentication modes for inbound provisioning calls: <code>KEYCLOAK</code> (Bearer access token), <code>EXTERNAL_JWT</code> (validated against an upstream issuer + JWKS), <code>EXTERNAL_SECRET</code> (shared bearer secret), and <code>EXTERNAL_BASIC</code> (HTTP Basic). Cleartext secrets and passwords are hashed with Argon2id before persistence.</li>
<li class="">Optional federation to the organization's configured identity provider, so provisioned users can immediately sign in via the org's existing SSO.</li>
</ul>
<p>The full surface is documented in <a href="https://github.com/p2-inc/keycloak-orgs/blob/main/docs/scim.md" target="_blank" rel="noopener noreferrer" class=""><code>docs/scim.md</code></a> in the keycloak-orgs repo.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-experimental-means-here">What "experimental" means here<a href="https://phasetwo.io/blog/orgs-scim-experimental/#what-experimental-means-here" class="hash-link" aria-label="Direct link to What &quot;experimental&quot; means here" title="Direct link to What &quot;experimental&quot; means here" translate="no">​</a></h3>
<p>The API shape, the configuration schema, and the realm-level enablement flag may change in a backwards-incompatible way before we mark it stable. If you take a dependency, pin to a specific version of <code>keycloak-orgs</code> and read the changelog before upgrading.</p>
<p>That caveat isn't theoretical — we're still iterating on the authentication mode set, the configuration shape for credential rotation, and the exact semantics around the IdP linkage. We'd rather get those right than freeze them prematurely.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="credit-where-its-due-metatavus-keycloak-scim-server">Credit where it's due: Metatavu's keycloak-scim-server<a href="https://phasetwo.io/blog/orgs-scim-experimental/#credit-where-its-due-metatavus-keycloak-scim-server" class="hash-link" aria-label="Direct link to Credit where it's due: Metatavu's keycloak-scim-server" title="Direct link to Credit where it's due: Metatavu's keycloak-scim-server" translate="no">​</a></h2>
<p>The SCIM 2.0 server implementation underneath this is <a href="https://github.com/Metatavu/keycloak-scim-server" target="_blank" rel="noopener noreferrer" class="">Metatavu's <code>keycloak-scim-server</code></a> — an open-source SCIM provider that handles the protocol-level work of speaking SCIM 2.0 to upstream IdPs. We've been building on top of it, contributing back, and maintaining a fork tuned for the per-organization model.</p>
<p>Metatavu's project is itself under active development, and so is our use of it. The two efforts are happening in parallel: Metatavu is iterating on the core SCIM server, and we're iterating on the organization-aware layer that sits on top. Neither codebase is "done."</p>
<p>What both projects do have, despite the experimental label, is <strong>active production users today</strong>. Metatavu has been running their SCIM server in real deployments, and Phase Two customers are already using the per-organization variant against real upstream IdPs. The "experimental" tag reflects the rate of change in the API surface, not the maturity of the underlying provisioning logic.</p>
<p>If you're evaluating this for a production rollout, the realistic posture is: it works, it's being used, and you should expect to update your integration when the configuration shape evolves. Pin versions and follow the changelog.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="getting-started">Getting started<a href="https://phasetwo.io/blog/orgs-scim-experimental/#getting-started" class="hash-link" aria-label="Direct link to Getting started" title="Direct link to Getting started" translate="no">​</a></h2>
<p>If you're already running Phase Two's Keycloak extensions, upgrade to the latest <code>keycloak-orgs</code>, enable the realm-level <code>scimEnabled</code> flag, and the SCIM tab will appear on each organization. The <a href="https://github.com/p2-inc/keycloak-orgs/blob/main/docs/scim.md" target="_blank" rel="noopener noreferrer" class="">SCIM docs</a> walk through the realm flag, the per-organization configuration, and each of the four authentication modes.</p>
<p>If you want to skip the self-host setup, you can try this on a free realm in the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> — the same Organization SCIM capability is available there.</p>
<p>Feedback, bug reports, and use-case stories are all welcome on the <a href="https://github.com/p2-inc/keycloak-orgs/issues" target="_blank" rel="noopener noreferrer" class="">keycloak-orgs issue tracker</a> or directly to <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>. The faster we hear from real integrations, the faster we get this to stable.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>organizations</category>
            <category>scim</category>
            <category>provisioning</category>
            <category>open_source</category>
        </item>
        <item>
            <title><![CDATA[A New Keycloak Theme Experience: Login, Admin, Account, and Email]]></title>
            <link>https://phasetwo.io/blog/new-keycloak-themes/</link>
            <guid>https://phasetwo.io/blog/new-keycloak-themes/</guid>
            <pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We rebuilt our Keycloak themes from the ground up using Keycloakify and shadcn/ui—delivering a modern login experience, a richer admin console, a polished account portal, and fully branded emails, all configurable at runtime without redeployment.]]></description>
            <content:encoded><![CDATA[<p>Keycloak theming has always been a pain point. The default themes that come with Keycloak leave a lot to be desired stylistically and cannot be customized easily. We have maintained our own set of disparate custom themes for the login, email and admin consoles but that has led to a maintenance nightmare and a disjointed user experience.</p>
<p>We've completely rebuilt our bundled Keycloak themes. What used to live as a tangle of custom pages inside a forked Keycloak repository is now a first-class <a href="https://www.keycloakify.dev/" target="_blank" rel="noopener noreferrer" class="">Keycloakify</a>-based React application that ships four themes: <strong>login</strong>, <strong>admin</strong>, <strong>account</strong>, and <strong>email</strong>. The result is faster to maintain, far more capable, and dramatically better out of the box for the organizations using Phase Two today.</p>
<p>Starting now, <strong>all Phase Two containers ship with this theme bundled</strong>. Any realm you create through the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> automatically gets the new login, admin, account, and email themes active—no configuration required. The first time a user hits your login page or receives an email from your realm, it already looks good.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-we-did-it">Why We Did It<a href="https://phasetwo.io/blog/new-keycloak-themes/#why-we-did-it" class="hash-link" aria-label="Direct link to Why We Did It" title="Direct link to Why We Did It" translate="no">​</a></h2>
<p>The old setup worked, but it accumulated debt fast. We maintained a fork of the Keycloak repository with admin UI customizations living on branches named things like <code>26.3.0_orgs_adminui</code>. Every Keycloak release meant rebasing those changes, resolving conflicts across thousands of lines of upstream JavaScript, and hoping nothing subtle broke. Testing required spinning up a full Keycloak image just to verify a button label.</p>
<p>Beyond the maintenance burden, the old approach had strong limits on what we could do. The login theme was limited by what you could do with PatternFly (RedHat's design framework). The email theme was limited to just overrides but you couldn't easily brand it. The admin console tweaks were limited to what we could fit into the existing UI without breaking things. And we had not even touched the account theme, which meant users had a completely different experience once they logged in. Lastly, when users jumped from our dashboard into the Keycloak admin console, the experience was jarring because it didn't reflect the product they had just been using.</p>
<p>We wanted:</p>
<ul>
<li class=""><strong>Maintainability</strong> that didn't require tracking every Keycloak release by hand</li>
<li class=""><strong>Runtime configurability</strong> so customers can change logos and colors easily without needing to redeploy or write custom themes</li>
<li class=""><strong>A richer admin experience</strong> for managing organizations, members, roles, and styles</li>
<li class=""><strong>A modern, polished look</strong> that doesn't feel like enterprise software from 2015</li>
<li class=""><strong>A proper dashboard</strong> for navigating what Phase Two adds on top of Keycloak</li>
<li class=""><strong>Modern fonts, layouts, and components</strong> that look good. Something about the RedHat fonts didn't seem to have the polish that most modern products have</li>
</ul>
<p>A combination of Keycloakify and Phase Two's large extension capabilities made it possible to build this without needing to fork Keycloak or maintain custom JARs with static templates.</p>
<p>If you want to start trying it now, grab our latest image from <a href="https://quay.io/repository/phasetwo/phasetwo-keycloak" target="_blank" rel="noopener noreferrer" class="">Quay.io</a> or try it out on the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-stack">The Stack<a href="https://phasetwo.io/blog/new-keycloak-themes/#the-stack" class="hash-link" aria-label="Direct link to The Stack" title="Direct link to The Stack" translate="no">​</a></h2>
<p>The new theme, called <code>phasetwo-ui</code>, is a React application built with:</p>
<ul>
<li class=""><strong>Keycloakify 11</strong> — bridges React components to Keycloak's theme SPI, handles JAR packaging automatically</li>
<li class=""><strong>shadcn/ui + Tailwind CSS 4</strong> — modern, composable component library for the login and account themes</li>
<li class=""><strong>PatternFly 5</strong> — used in the admin and account console to avoid a complete rewrite of those complex interfaces while still allowing us to inject our own styles and components</li>
<li class=""><strong>Vite</strong> — fast builds and hot module replacement during development</li>
<li class=""><strong>Storybook</strong> — isolated component development and visual testing</li>
<li class=""><strong>Playwright</strong> — end-to-end tests that exercise the admin UI against a real Keycloak instance</li>
</ul>
<p>The Java side extends the Keycloak SPI to handle runtime theme loading, dynamic CSS generation, Mustache template rendering, and a configurable email sender. All of it ships as a single JAR dropped into the Keycloak <code>providers/</code> directory.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="login-theme">Login Theme<a href="https://phasetwo.io/blog/new-keycloak-themes/#login-theme" class="hash-link" aria-label="Direct link to Login Theme" title="Direct link to Login Theme" translate="no">​</a></h2>
<p>Out of the box, Keycloak's default login pages are functional but bare. The new <code>phasetwo-ui</code> login theme replaces them entirely with something that looks like a product, not a framework demo. It's a two-column layout with the branding panel on the right on desktop and a mobile-optimized single-column view on smaller screens. It supports all 21+ standard Keycloak login flows: standard authentication, registration, password reset, TOTP setup, WebAuthn enrollment, account deletion, email verification, identity provider linking, OAuth consent, and error handling.</p>
<p>For new Phase Two realms, this is now the default. Your users will see this from day one.</p>
<p><img decoding="async" loading="lazy" alt="Login Theme" src="https://phasetwo.io/assets/images/login-520f3113e3ce0a5e2d5a6f7012c81408.png" width="3112" height="1712" class="img_R7yg"></p>
<p>The bigger story is runtime configurability. Every visual element of the login page is controlled by realm attributes—no redeploy required:</p>
<ul>
<li class=""><strong>Logo</strong> — set a URL and it shows up immediately</li>
<li class=""><strong>Favicon and app icon</strong> — same approach, including PWA-ready app icons</li>
<li class=""><strong>Primary, secondary, and background colors</strong> — picked in the admin UI, reflected instantly in login</li>
<li class=""><strong>Custom CSS</strong> — inject arbitrary overrides without touching templates</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Login Theme Customized" src="https://phasetwo.io/assets/images/login-theme-customized-51c0a7c60797e719f40ad295811d0547.png" width="3898" height="1678" class="img_R7yg"></p>
<p>These attributes are served through a dynamic <code>/realms/&lt;realm&gt;/assets/css/login.css</code> endpoint that the Java SPI generates on the fly, mapping realm attributes to the right CSS variables.</p>
<p>Dark mode is supported. Language switching is built in. Fallback logos handle the case where a custom logo URL fails to load.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="admin-theme">Admin Theme<a href="https://phasetwo.io/blog/new-keycloak-themes/#admin-theme" class="hash-link" aria-label="Direct link to Admin Theme" title="Direct link to Admin Theme" translate="no">​</a></h2>
<p>This is where the most work went, some visual but a lot of it underlying plumbing. The admin theme extends Keycloak's native admin console with a dedicated <strong>Phase Two</strong> panel that surfaces everything our extensions add.</p>
<p><img decoding="async" loading="lazy" alt="Admin Theme" src="https://phasetwo.io/assets/images/admin-theme-c15ee3752ea7444e9771c11f42ab1ade.png" width="3046" height="1818" class="img_R7yg"></p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="organizations">Organizations<a href="https://phasetwo.io/blog/new-keycloak-themes/#organizations" class="hash-link" aria-label="Direct link to Organizations" title="Direct link to Organizations" translate="no">​</a></h3>
<p>The organizations section is a full CRUD interface for everything the <a href="https://github.com/p2-inc/phasetwo-keycloak" target="_blank" rel="noopener noreferrer" class="">Keycloak Organizations extension</a> provides:</p>
<ul>
<li class=""><strong>Organization list</strong> with creation and deletion</li>
<li class=""><strong>Organization details</strong> — name, description, display name, attributes</li>
<li class=""><strong>Members</strong> — add, remove, view member details and organization-scoped attributes</li>
<li class=""><strong>Roles</strong> — create and manage roles that are scoped to a specific organization</li>
<li class=""><strong>Identity Providers</strong> — assign IdPs to organizations and configure sync modes</li>
<li class=""><strong>Domains</strong> — domain-based routing configuration</li>
<li class=""><strong>Invitations</strong> — send and manage membership invitations by email</li>
<li class=""><strong>Settings</strong> — enabled/disabled state and other org-level configuration</li>
</ul>
<p>The API calls are fully typed against our OpenAPI schema, so the UI and the API stay in sync automatically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="custom-styles">Custom Styles<a href="https://phasetwo.io/blog/new-keycloak-themes/#custom-styles" class="hash-link" aria-label="Direct link to Custom Styles" title="Direct link to Custom Styles" translate="no">​</a></h3>
<p>The styles panel gives realm administrators direct control over branding without writing any code. It has four tabs:</p>
<ol>
<li class=""><strong>General</strong> — upload URLs for logo, favicon, and app icon with live image previews</li>
<li class=""><strong>Login</strong> — color pickers for primary, secondary, and background colors plus a CSS editor for arbitrary overrides</li>
<li class=""><strong>Email</strong> — customize email template branding including footer text</li>
<li class=""><strong>Portal</strong> — portal-specific styling controls for the Admin Portal, a self-serve application for end users to manage their account and Organization memberships without needing access to the full Keycloak admin console</li>
</ol>
<p>Everything saves directly to realm attributes. The login page picks up changes immediately because the CSS endpoint reads those attributes on every request.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="account-theme">Account Theme<a href="https://phasetwo.io/blog/new-keycloak-themes/#account-theme" class="hash-link" aria-label="Direct link to Account Theme" title="Direct link to Account Theme" translate="no">​</a></h2>
<p>The account theme wraps Keycloak's standard account management UI in the same visual language as the login and admin themes. Users get a consistent branded experience whether they're logging in, managing their sessions, or updating their profile. A session expiration warning overlay prevents users from losing unsaved changes mid-session.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="email-theme">Email Theme<a href="https://phasetwo.io/blog/new-keycloak-themes/#email-theme" class="hash-link" aria-label="Direct link to Email Theme" title="Direct link to Email Theme" translate="no">​</a></h2>
<p>The default Keycloak email templates are plain text in a minimal HTML shell. They get the job done, but they don't reflect well on the product they're sending from. Our new email theme replaces that with a professional, fully branded layout—clean typography, your logo embedded directly in the message, a structured content area with appropriate padding and shadow, and a configurable footer. All coupled with an in-page preview experience. Emails from your realm will look like they came from a real product from day one.</p>
<p>For realms created through the Phase Two Dashboard, this is now the default. No setup required.</p>
<p><img decoding="async" loading="lazy" alt="Email Theme" src="https://phasetwo.io/assets/images/email-theme-configuration-2b4f8ff91b44e39c35c7aebfe6819d2c.png" width="2656" height="2080" class="img_R7yg"></p>
<p>Beyond the out-of-the-box look, we addressed the deeper problem: email was arguably the most rigid part of the old setup. Static Freemarker templates meant any copy or branding change required a build and deploy. We replaced that with two mechanisms.</p>
<p><strong>Attribute-based template overrides</strong> — any <code>.ftl</code> template can be overridden by setting a realm attribute with the key <code>_providerConfig.theme.email.templates.&lt;template-name.ftl&gt;</code>. Message strings work the same way via <code>_providerConfig.theme.email.messages.&lt;message-key&gt;</code>. No redeploy, no file system access required.</p>
<p><strong>Mustache template support</strong> — we ship an alternative <code>EmailTemplateProvider</code> implementation that renders Mustache templates in addition to Freemarker. This opens up a simpler, more readable template syntax and lets customers supply their own templates through the admin UI without needing to understand Freemarker's quirks.</p>
<p>The email layout is configurable without touching templates: set footer text lines and a logo URL from the <strong>Phase Two → Styles → Email</strong> panel in the admin console and they reflect immediately.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-dashboard">The Dashboard<a href="https://phasetwo.io/blog/new-keycloak-themes/#the-dashboard" class="hash-link" aria-label="Direct link to The Dashboard" title="Direct link to The Dashboard" translate="no">​</a></h2>
<p>One of the additions we're most excited about is the Phase Two Dashboard—a dedicated view in the admin console that gives realm administrators a high-level picture of their deployment at a glance. Organizations, members, and activity stats surface immediately without having to drill into individual sections. It's the kind of overview that makes Keycloak feel less like a configuration interface and more like a product. We have started basic here, but will continue to expand the dashboard with more insights and actionable items over time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="easier-to-maintain">Easier to Maintain<a href="https://phasetwo.io/blog/new-keycloak-themes/#easier-to-maintain" class="hash-link" aria-label="Direct link to Easier to Maintain" title="Direct link to Easier to Maintain" translate="no">​</a></h2>
<p>The practical benefit for us is that upgrading Keycloak no longer means rebasing thousands of lines of forked admin UI JavaScript. Keycloakify handles the bridge between our React components and the Keycloak theme SPI. When Keycloak's admin UI packages update, we update our Keycloakify dependencies. Our customizations live in well-defined extension points, not in modified copies of upstream files.</p>
<p>We also get proper tooling for the first time: Storybook for developing components in isolation, Playwright tests that exercise the admin UI against a real Keycloak instance, and a <code>docker compose up</code> local environment that lets any developer work on the theme without needing deep Keycloak internals knowledge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-to-get-it">How to Get It<a href="https://phasetwo.io/blog/new-keycloak-themes/#how-to-get-it" class="hash-link" aria-label="Direct link to How to Get It" title="Direct link to How to Get It" translate="no">​</a></h2>
<p><strong>Phase Two managed service</strong> — it's already there. All containers now ship with the <code>phasetwo-ui</code> theme bundled. New realms created through the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> have it active automatically. Existing realms can switch by setting <code>phasetwo-ui</code> as the theme for login, admin, account, and email in realm settings, then configuring branding from the <strong>Phase Two → Styles</strong> panel.</p>
<p><strong>Self-hosted with the Phase Two image</strong> — the theme is included in the <a href="https://quay.io/repository/phasetwo/phasetwo-keycloak" target="_blank" rel="noopener noreferrer" class="">Phase Two Keycloak image</a>. Pull the latest image and select <code>phasetwo-ui</code> in your realm theme settings.</p>
<p><strong>Self-hosted with your own Keycloak</strong> — build and install from the open source <a href="https://github.com/p2-inc/keycloak-themes" target="_blank" rel="noopener noreferrer" class="">keycloak-themes</a> repository:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mvn clean install -DskipTests</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">cp target/keycloak-themes-*.jar /path/to/keycloak/providers/</span><br></div></code></pre></div></div>
<p>Restart Keycloak and select <code>phasetwo-ui</code> for login, admin, account, and email in your realm configuration.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="looking-forward">Looking Forward<a href="https://phasetwo.io/blog/new-keycloak-themes/#looking-forward" class="hash-link" aria-label="Direct link to Looking Forward" title="Direct link to Looking Forward" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="visual-editor-for-login-and-email">Visual editor for login and email<a href="https://phasetwo.io/blog/new-keycloak-themes/#visual-editor-for-login-and-email" class="hash-link" aria-label="Direct link to Visual editor for login and email" title="Direct link to Visual editor for login and email" translate="no">​</a></h3>
<p>The styles panel today gives you color pickers, a logo field, and a CSS editor. That covers a lot, but we want to go further. We're building a visual editor directly into the Phase Two Dashboard that lets you make fine-grained design changes to your login and email themes—adjusting layout, spacing, typography, background images, and component styles through a live preview interface rather than writing CSS by hand.</p>
<p>The goal isn't to replace a fully bespoke login experience. Some brands need total control, and the custom CSS path will always be there for that. But for the vast majority of teams, getting 95% of the way there without a designer or a build pipeline is exactly what's needed. Your login page should feel like it belongs to your product, and we think that should be achievable in an afternoon, not a sprint.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="more-capability-in-the-admin-and-account-uis">More capability in the Admin and Account UIs<a href="https://phasetwo.io/blog/new-keycloak-themes/#more-capability-in-the-admin-and-account-uis" class="hash-link" aria-label="Direct link to More capability in the Admin and Account UIs" title="Direct link to More capability in the Admin and Account UIs" translate="no">​</a></h3>
<p>We're continuing to expand what the admin and account UIs can do. On the admin side, more of our extension capabilities will surface directly in the Phase Two panel as we add features—so you're managing everything through the console rather than through API calls or raw attribute editing.</p>
<p>On the account side, we're reworking several views to feel more like a modern product interface. One early change is moving the account list views from a table layout to a tile-based interface—easier to scan, more room for useful metadata, more familiar UX, and a look that fits better alongside the rest of the theme.</p>
<p>For email themes, we're adding more template variables and making it easier to customize the content and layout of emails without needing to write custom templates.</p>
<p>If you have feedback or run into anything, open an issue on <a href="https://github.com/p2-inc/keycloak-themes" target="_blank" rel="noopener noreferrer" class="">GitHub</a> or <a href="https://phasetwo.io/contact" target="_blank" rel="noopener noreferrer" class="">drop us a line</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="need-help-customizing-your-keycloak-experience">Need help customizing your Keycloak experience?<a href="https://phasetwo.io/blog/new-keycloak-themes/#need-help-customizing-your-keycloak-experience" class="hash-link" aria-label="Direct link to Need help customizing your Keycloak experience?" title="Direct link to Need help customizing your Keycloak experience?" translate="no">​</a></h2>
<p>Whether you're trying to match your brand exactly, integrate a custom identity flow, or just figure out where to start—we're happy to help. <a href="https://phasetwo.io/contact" target="_blank" rel="noopener noreferrer" class="">Reach out to us</a> and we'll point you in the right direction.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>themes</category>
            <category>open_source</category>
            <category>admin-ui</category>
            <category>keycloakify</category>
            <category>organizations</category>
            <category>branding</category>
        </item>
        <item>
            <title><![CDATA[Instant MCP authorization using Keycloak]]></title>
            <link>https://phasetwo.io/blog/instant-mcp-authorization-keycloak/</link>
            <guid>https://phasetwo.io/blog/instant-mcp-authorization-keycloak/</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Protect an MCP server with Keycloak, wire it into VS Code using dynamic client registration, and get a working local setup quickly.]]></description>
            <content:encoded><![CDATA[<p>If you are exposing tools over MCP, you usually do not want every client on the network calling them anonymously. Even for a local prototype, you typically want a real login flow, consent, scoped access tokens, and a clean way to validate who is allowed to run what.</p>
<p>Keycloak is the easiest way to do that without inventing your own authorization layer. It already handles browser login, consent, token issuance, JWKS discovery, and OAuth metadata. Your MCP server just needs to behave like a protected resource and validate bearer tokens correctly.</p>
<p>In this guide, we will build a tiny calculator MCP server in Python, protect it with Keycloak, and connect to it from VS Code using Dynamic Client Registration (DCR). By the end, VS Code will open a browser to Keycloak, you will sign in, approve access to the <code>mcp:run</code> scope, and then call your MCP tools directly from chat.</p>
<p>This walkthrough uses:</p>
<ul>
<li class="">The official MCP Python SDK</li>
<li class="">One MCP endpoint at <code>http://localhost:8000/mcp</code></li>
<li class="">Keycloak as the authorization server</li>
<li class="">A Keycloak client scope named <code>mcp:run</code></li>
<li class="">An Audience mapper that sets <code>aud = http://localhost:8000/mcp</code></li>
<li class=""><code>Include in token scope</code> enabled for <code>mcp:run</code></li>
<li class="">Trusted Hosts removed from anonymous client registration policies so VS Code's DCR request is accepted</li>
</ul>
<p>If you do not want to host and configure Keycloak yourself, you can create a free realm in the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> and use that instead of a self-hosted local Keycloak instance. The rest of the MCP-side setup stays the same.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-you-are-building">What you are building<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#what-you-are-building" class="hash-link" aria-label="Direct link to What you are building" title="Direct link to What you are building" translate="no">​</a></h2>
<p>We will create a small MCP server that exposes three tools:</p>
<ul>
<li class=""><code>add_numbers</code></li>
<li class=""><code>multiply_numbers</code></li>
<li class=""><code>divide_numbers</code></li>
</ul>
<p>The server will run locally and expose its MCP endpoint at:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">http://localhost:8000/mcp</span><br></div></code></pre></div></div>
<p>VS Code will connect to that URL, discover the MCP server's protected-resource metadata, discover Keycloak as the authorization server, dynamically register itself, send you through the browser login flow, and then call the tools with an access token.</p>
<p>That is the key mental model: the MCP server is the protected resource, and Keycloak is the authorization server.</p>
<p>A high-level diagram of the topology looks like this:</p>
<p><img decoding="async" loading="lazy" alt="MCP Keycloak Topology" src="https://phasetwo.io/assets/images/dataflow-topology-260c20ed19273edc256901c1c12c04b9.png" width="2758" height="1376" class="img_R7yg"></p>
<p>with data flowing through it like this:</p>
<p><img decoding="async" loading="lazy" alt="MCP Keycloak Dataflow Sequence" src="https://phasetwo.io/assets/images/dataflow-sequence-24be5b0ff9b6ab97ad0923f5e284c423.png" width="2764" height="1804" class="img_R7yg"></p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-you-need-installed">What you need installed<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#what-you-need-installed" class="hash-link" aria-label="Direct link to What you need installed" title="Direct link to What you need installed" translate="no">​</a></h2>
<p>Install these first:</p>
<ul>
<li class="">Python 3.11 or newer</li>
<li class="">Docker Desktop</li>
<li class="">VS Code</li>
<li class="">GitHub Copilot or another model-enabled chat workflow in VS Code</li>
</ul>
<p>Then create a virtual environment and install dependencies:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mkdir keycloak-mcp-demo</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd keycloak-mcp-demo</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python -m venv .venv</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">source .venv/bin/activate</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pip install "mcp[cli]" "pyjwt[crypto]&gt;=2.8.0"</span><br></div></code></pre></div></div>
<p>This setup uses the official MCP Python SDK and PyJWT to validate Keycloak-issued access tokens.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-the-auth-flow-works">How the auth flow works<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#how-the-auth-flow-works" class="hash-link" aria-label="Direct link to How the auth flow works" title="Direct link to How the auth flow works" translate="no">​</a></h2>
<p>When VS Code first hits your MCP endpoint, it does not already have a token. The server should reject that first request with <code>401 Unauthorized</code> and include a <code>WWW-Authenticate</code> header pointing to a protected-resource metadata document.</p>
<p>VS Code then:</p>
<ol>
<li class="">Fetches the protected-resource metadata</li>
<li class="">Learns which authorization server protects this MCP server</li>
<li class="">Discovers the authorization server metadata from Keycloak</li>
<li class="">Dynamically registers a public client</li>
<li class="">Opens your browser for login and consent</li>
<li class="">Gets an access token back</li>
<li class="">Retries the MCP request with <code>Authorization: Bearer ...</code></li>
</ol>
<p>For this to work, two endpoints have to behave correctly:</p>
<ul>
<li class=""><code>GET /mcp</code> must return a <code>401</code> challenge when no token is present</li>
<li class=""><code>GET /.well-known/oauth-protected-resource/mcp</code> must advertise the Keycloak realm and the exact MCP resource URL</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-1-create-the-mcp-server">Step 1: Create the MCP server<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-1-create-the-mcp-server" class="hash-link" aria-label="Direct link to Step 1: Create the MCP server" title="Direct link to Step 1: Create the MCP server" translate="no">​</a></h2>
<p>Download the complete example server here:</p>
<ul>
<li class="">
<a href="https://phasetwo.io/blog/mcp_authorization_keycloak/keycloak-mcp-server.py">server.py</a>
</li>
</ul>
<p>This server:</p>
<ul>
<li class="">Validates the token signature against Keycloak's JWKS</li>
<li class="">Checks the token issuer</li>
<li class="">Checks that the token audience matches the exact MCP URL</li>
<li class="">Checks that the <code>mcp:run</code> scope appears in the token's <code>scope</code> claim</li>
</ul>
<p>Start the server:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">python server.py</span><br></div></code></pre></div></div>
<p>That <code>scope</code> check matters. If <code>mcp:run</code> exists as a Keycloak client scope but is not included in token scope, the access token can come back with the correct audience but an empty <code>scope</code> claim, and the server will reject it. If you hit this, paste the access token into our <a class="" href="https://phasetwo.io/tools/jwt-decoder/">JWT Decoder</a> — it surfaces the <code>scope</code> claim and <code>aud</code> so you can see exactly what Keycloak emitted.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-2-verify-the-server-before-using-vs-code">Step 2: Verify the server before using VS Code<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-2-verify-the-server-before-using-vs-code" class="hash-link" aria-label="Direct link to Step 2: Verify the server before using VS Code" title="Direct link to Step 2: Verify the server before using VS Code" translate="no">​</a></h2>
<p>Before touching Keycloak or VS Code, make sure the server is advertising the right OAuth metadata.</p>
<p>Check the protected-resource metadata:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl -s http://localhost:8000/.well-known/oauth-protected-resource/mcp</span><br></div></code></pre></div></div>
<p>You should get something like:</p>
<div class="language-json codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-json codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"resource"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"http://localhost:8000/mcp"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"authorization_servers"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"http://localhost:8080/realms/mcp-demo"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"scopes_supported"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"mcp:run"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"bearer_methods_supported"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"header"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Then check the unauthenticated MCP endpoint:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl -i http://localhost:8000/mcp</span><br></div></code></pre></div></div>
<p>You should get <code>401 Unauthorized</code>, and the <code>WWW-Authenticate</code> header should point to:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">resource_metadata="http://localhost:8000/.well-known/oauth-protected-resource/mcp"</span><br></div></code></pre></div></div>
<p>If those two responses are correct, VS Code has the discovery information it needs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-3-start-keycloak-locally">Step 3: Start Keycloak locally<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-3-start-keycloak-locally" class="hash-link" aria-label="Direct link to Step 3: Start Keycloak locally" title="Direct link to Step 3: Start Keycloak locally" translate="no">​</a></h2>
<p>Run Keycloak in development mode:</p>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">docker run --rm \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  -p 127.0.0.1:8080:8080 \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  quay.io/keycloak/keycloak start-dev</span><br></div></code></pre></div></div>
<p>Then open:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">http://localhost:8080</span><br></div></code></pre></div></div>
<p>Sign in as:</p>
<ul>
<li class="">Username: <code>admin</code></li>
<li class="">Password: <code>admin</code></li>
</ul>
<p>If you want to skip self-hosting, create a free realm in the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> instead. For this tutorial, that is usually the fastest route because you avoid local Keycloak setup, bootstrap admin credentials, and one-off realm configuration from scratch.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-4-create-the-realm-and-a-test-user">Step 4: Create the realm and a test user<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-4-create-the-realm-and-a-test-user" class="hash-link" aria-label="Direct link to Step 4: Create the realm and a test user" title="Direct link to Step 4: Create the realm and a test user" translate="no">​</a></h2>
<p>Create a realm named:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mcp-demo</span><br></div></code></pre></div></div>
<p>Then create a test user such as <code>alice</code>, set a password, and make sure you can sign in with it.</p>
<p>The realm issuer your server expects is:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">http://localhost:8080/realms/mcp-demo</span><br></div></code></pre></div></div>
<p>That exact URL must match the issuer your Python server validates.</p>
<p>If you want a faster setup than clicking through each screen manually, you can also import the example realm configuration:</p>
<ul>
<li class="">
<a href="https://phasetwo.io/blog/mcp_authorization_keycloak/keycloak-realm-export.json">realm-export.json</a>
</li>
</ul>
<p>If you are using a hosted realm from Phase Two instead of local Keycloak, use the hosted realm's issuer URL everywhere the tutorial currently shows <code>http://localhost:8080/realms/mcp-demo</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-5-create-the-mcprun-client-scope">Step 5: Create the <code>mcp:run</code> client scope<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-5-create-the-mcprun-client-scope" class="hash-link" aria-label="Direct link to step-5-create-the-mcprun-client-scope" title="Direct link to step-5-create-the-mcprun-client-scope" translate="no">​</a></h2>
<p>Create a new client scope named:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mcp:run</span><br></div></code></pre></div></div>
<p>Then configure it like this:</p>
<ul>
<li class=""><code>Include in token scope</code>: ON</li>
<li class=""><code>Display on consent screen</code>: ON is fine</li>
</ul>
<p>That first setting is essential for this implementation. The server checks the access token's <code>scope</code> claim, so Keycloak must actually emit <code>mcp:run</code> into the token.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="add-the-audience-mapper">Add the Audience mapper<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#add-the-audience-mapper" class="hash-link" aria-label="Direct link to Add the Audience mapper" title="Direct link to Add the Audience mapper" translate="no">​</a></h3>
<p>Inside <code>mcp:run</code>, add an Audience mapper with:</p>
<ul>
<li class=""><code>Included Custom Audience</code>: <code>http://localhost:8000/mcp</code></li>
<li class=""><code>Add to access token</code>: ON</li>
<li class=""><code>Add to introspection</code>: ON is fine</li>
<li class=""><code>Add to ID token</code>: OFF is fine</li>
</ul>
<p>This is what causes the access token to carry:</p>
<div class="language-json codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-json codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token property">"aud"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"http://localhost:8000/mcp"</span><br></div></code></pre></div></div>
<p>That audience must exactly match the MCP server URL the Python server expects.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-6-make-mcprun-available-to-dcr-created-clients">Step 6: Make <code>mcp:run</code> available to DCR-created clients<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-6-make-mcprun-available-to-dcr-created-clients" class="hash-link" aria-label="Direct link to step-6-make-mcprun-available-to-dcr-created-clients" title="Direct link to step-6-make-mcprun-available-to-dcr-created-clients" translate="no">​</a></h2>
<p>For VS Code DCR to work smoothly, dynamically registered public clients must be able to get the <code>mcp:run</code> scope without any manual client editing.</p>
<p>There are two places to check.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="add-mcprun-to-the-realms-default-client-scopes">Add <code>mcp:run</code> to the realm's default client scopes<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#add-mcprun-to-the-realms-default-client-scopes" class="hash-link" aria-label="Direct link to add-mcprun-to-the-realms-default-client-scopes" title="Direct link to add-mcprun-to-the-realms-default-client-scopes" translate="no">​</a></h3>
<p>Make sure <code>mcp:run</code> is included in the realm's default client scopes.</p>
<p>That way, a newly registered public client will inherit it automatically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="allow-mcprun-in-anonymous-client-registration">Allow <code>mcp:run</code> in anonymous client registration<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#allow-mcprun-in-anonymous-client-registration" class="hash-link" aria-label="Direct link to allow-mcprun-in-anonymous-client-registration" title="Direct link to allow-mcprun-in-anonymous-client-registration" translate="no">​</a></h3>
<p>In the realm's client registration policies, keep the anonymous Allowed Client Scopes policy and make sure it includes:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">mcp:run</span><br></div></code></pre></div></div>
<p>That tells Keycloak that anonymous DCR clients are allowed to request the MCP scope.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-7-remove-the-trusted-hosts-policy">Step 7: Remove the Trusted Hosts policy<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-7-remove-the-trusted-hosts-policy" class="hash-link" aria-label="Direct link to Step 7: Remove the Trusted Hosts policy" title="Direct link to Step 7: Remove the Trusted Hosts policy" translate="no">​</a></h2>
<p>This is the piece that tends to block VS Code.</p>
<p>VS Code's DCR request includes multiple URLs, including redirect URIs for <code>vscode.dev</code>, <code>insiders.vscode.dev</code>, <code>localhost</code>, and <code>127.0.0.1</code>, plus a client URI for the VS Code website. Keycloak's default Trusted Hosts registration policy rejects that request.</p>
<p>For this local tutorial setup, remove the Trusted Hosts policy from the realm's anonymous client-registration policies.</p>
<p>That is what allows VS Code's DCR payload to succeed.</p>
<p>You can keep the other anonymous registration policies in place, especially:</p>
<ul>
<li class="">Allowed Client Scopes</li>
<li class="">Allowed Registration Web Origins</li>
<li class="">Allowed Protocol Mapper Types</li>
<li class="">Consent Required</li>
<li class="">Max Clients Limit</li>
</ul>
<p>But Trusted Hosts needs to go for this workflow.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-8-configure-vs-code">Step 8: Configure VS Code<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-8-configure-vs-code" class="hash-link" aria-label="Direct link to Step 8: Configure VS Code" title="Direct link to Step 8: Configure VS Code" translate="no">​</a></h2>
<p>Create <code>.vscode/mcp.json</code>, or download the example file:</p>
<ul>
<li class="">
<a href="https://phasetwo.io/blog/mcp_authorization_keycloak/keycloak-mcp.json">mcp.json</a>
</li>
</ul>
<p>The contents should look like:</p>
<div class="language-json codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-json codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"servers"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"calculator-local"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"type"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"http"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"url"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"http://localhost:8000/mcp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Then in VS Code:</p>
<ol>
<li class="">Open the project</li>
<li class="">Open the Command Palette</li>
<li class="">Run <code>MCP: List Servers</code></li>
<li class="">Confirm <code>calculator-local</code> appears</li>
<li class="">Start it</li>
</ol>
<p>If you previously had failed auth attempts cached, run:</p>
<ul>
<li class=""><code>Authentication: Remove Dynamic Authentication Providers</code></li>
</ul>
<p>Then restart VS Code before trying again.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-9-sign-in-through-keycloak">Step 9: Sign in through Keycloak<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#step-9-sign-in-through-keycloak" class="hash-link" aria-label="Direct link to Step 9: Sign in through Keycloak" title="Direct link to Step 9: Sign in through Keycloak" translate="no">​</a></h2>
<p>Once VS Code connects to the server, it should:</p>
<ol>
<li class="">Discover the protected-resource metadata at <code>/.well-known/oauth-protected-resource/mcp</code></li>
<li class="">Discover Keycloak as the authorization server</li>
<li class="">Dynamically register a public client</li>
<li class="">Open your browser</li>
<li class="">Send you to Keycloak login</li>
<li class="">Show a consent prompt for <code>mcp:run</code></li>
<li class="">Redirect back to VS Code</li>
<li class="">Retry the MCP request with a bearer token</li>
</ol>
<p>If all the values are aligned, the server will accept the token and VS Code will discover the three tools.</p>
<p>At that point you can try a prompt such as:</p>
<div class="language-text codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-text codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">Use calculator-local to add 12.5 and 7.25, then multiply the result by 3.</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-to-do-if-it-still-fails">What to do if it still fails<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#what-to-do-if-it-still-fails" class="hash-link" aria-label="Direct link to What to do if it still fails" title="Direct link to What to do if it still fails" translate="no">​</a></h2>
<p>If the browser flow works but the MCP server still responds with <code>401</code>, the next thing to check is the token itself.</p>
<p>In practice, there are only three claims that matter most here:</p>
<ul>
<li class=""><code>iss</code></li>
<li class=""><code>aud</code></li>
<li class=""><code>scope</code></li>
</ul>
<p>Your server expects:</p>
<ul>
<li class=""><code>iss = http://localhost:8080/realms/mcp-demo</code></li>
<li class=""><code>aud = http://localhost:8000/mcp</code></li>
<li class=""><code>scope</code> contains <code>mcp:run</code></li>
</ul>
<p>The most common problems are:</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="aud-is-wrong"><code>aud</code> is wrong<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#aud-is-wrong" class="hash-link" aria-label="Direct link to aud-is-wrong" title="Direct link to aud-is-wrong" translate="no">​</a></h3>
<p>The Audience mapper is missing or its custom audience value does not exactly match the MCP URL.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="scope-is-empty"><code>scope</code> is empty<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#scope-is-empty" class="hash-link" aria-label="Direct link to scope-is-empty" title="Direct link to scope-is-empty" translate="no">​</a></h3>
<p><code>mcp:run</code> exists as a client scope, but <code>Include in token scope</code> is still off.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="dcr-fails-before-login">DCR fails before login<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#dcr-fails-before-login" class="hash-link" aria-label="Direct link to DCR fails before login" title="Direct link to DCR fails before login" translate="no">​</a></h3>
<p>The anonymous client registration policies still block VS Code's DCR payload. The first place to check is whether Trusted Hosts was actually removed.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="vs-code-behaves-inconsistently">VS Code behaves inconsistently<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#vs-code-behaves-inconsistently" class="hash-link" aria-label="Direct link to VS Code behaves inconsistently" title="Direct link to VS Code behaves inconsistently" translate="no">​</a></h3>
<p>Clear stale auth state with:</p>
<ul>
<li class=""><code>Authentication: Remove Dynamic Authentication Providers</code></li>
</ul>
<p>Then retry.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="a-few-implementation-notes-worth-remembering">A few implementation notes worth remembering<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#a-few-implementation-notes-worth-remembering" class="hash-link" aria-label="Direct link to A few implementation notes worth remembering" title="Direct link to A few implementation notes worth remembering" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-mcp-server-is-not-the-identity-provider">The MCP server is not the identity provider<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#the-mcp-server-is-not-the-identity-provider" class="hash-link" aria-label="Direct link to The MCP server is not the identity provider" title="Direct link to The MCP server is not the identity provider" translate="no">​</a></h3>
<p>Your MCP server does not need to implement a full OAuth server. It only needs to behave like a protected resource:</p>
<ul>
<li class="">Advertise protected-resource metadata</li>
<li class="">Challenge unauthenticated requests with <code>401</code></li>
<li class="">Validate bearer tokens from Keycloak</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="vs-code-cares-about-the-metadata-not-just-the-token-endpoint">VS Code cares about the metadata, not just the token endpoint<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#vs-code-cares-about-the-metadata-not-just-the-token-endpoint" class="hash-link" aria-label="Direct link to VS Code cares about the metadata, not just the token endpoint" title="Direct link to VS Code cares about the metadata, not just the token endpoint" translate="no">​</a></h3>
<p>If the MCP endpoint does not return the right <code>401</code> challenge, or the protected-resource metadata does not point at the Keycloak realm correctly, VS Code will never get to DCR.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="audience-matching-is-exact">Audience matching is exact<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#audience-matching-is-exact" class="hash-link" aria-label="Direct link to Audience matching is exact" title="Direct link to Audience matching is exact" translate="no">​</a></h3>
<p><code>http://localhost:8000/mcp</code> is not the same as <code>http://localhost:8000/mcp/</code> or <code>http://localhost:8000/</code>.</p>
<p>Use the exact same value everywhere:</p>
<ul>
<li class="">In VS Code</li>
<li class="">In the server</li>
<li class="">In Keycloak's Audience mapper</li>
<li class="">In the protected-resource metadata</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="final-checklist">Final checklist<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#final-checklist" class="hash-link" aria-label="Direct link to Final checklist" title="Direct link to Final checklist" translate="no">​</a></h2>
<p>Before you call this done, make sure all of these match:</p>
<ul>
<li class="">VS Code MCP URL: <code>http://localhost:8000/mcp</code></li>
<li class="">Protected-resource metadata <code>resource</code>: <code>http://localhost:8000/mcp</code></li>
<li class="">Keycloak audience mapper custom audience: <code>http://localhost:8000/mcp</code></li>
<li class="">Python <code>MCP_SERVER_URL</code>: <code>http://localhost:8000/mcp</code></li>
<li class="">Required scope: <code>mcp:run</code></li>
<li class=""><code>mcp:run</code> has <code>Include in token scope</code> enabled</li>
<li class=""><code>mcp:run</code> is available to DCR-created clients</li>
<li class="">Anonymous Trusted Hosts policy is removed</li>
</ul>
<p>If those are true, you have a clean local setup for a Keycloak-protected MCP server that VS Code can discover, register against, and use.</p>
<p><img decoding="async" loading="lazy" alt="MCP Keycloak Dataflow" src="https://phasetwo.io/assets/images/dataflow-bb6b6541403751224ddd6744e6bf5877.png" width="3333" height="4374" class="img_R7yg"></p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-phase-two-is-the-easiest-way-to-run-this-for-real">Why Phase Two is the easiest way to run this for real<a href="https://phasetwo.io/blog/instant-mcp-authorization-keycloak/#why-phase-two-is-the-easiest-way-to-run-this-for-real" class="hash-link" aria-label="Direct link to Why Phase Two is the easiest way to run this for real" title="Direct link to Why Phase Two is the easiest way to run this for real" translate="no">​</a></h2>
<p>If you like this pattern but do not want to spend time standing up and operating Keycloak, Phase Two's hosted Keycloak offering is the fastest way to get there. You get a managed realm, a production-ready control plane, and a much shorter path from prototype to deployed MCP authorization server.</p>
<p>You can start with a free realm in the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dashboard</a> for testing, and when you are ready for a hosted setup with support, upgrades, and operational help, Phase Two's managed Keycloak offering is the easiest way to run an MCP authorization server without owning the infrastructure yourself.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>mcp</category>
            <category>authorization</category>
            <category>vscode</category>
            <category>tutorial</category>
            <category>open_source</category>
        </item>
        <item>
            <title><![CDATA[Phase Two Achieves ISO/IEC 27001 Certification]]></title>
            <link>https://phasetwo.io/blog/iso-27001-certification/</link>
            <guid>https://phasetwo.io/blog/iso-27001-certification/</guid>
            <pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two has achieved full ISO/IEC 27001 certification, strengthening our information security management program just over six months after our September 17, 2025 SOC 2 Type II compliance milestone.]]></description>
            <content:encoded><![CDATA[<p>Phase Two is excited to announce that we are now <strong>ISO/IEC 27001 certified</strong>.</p>
<p>This milestone reflects how seriously we take security and compliance across our platform, operations, and internal processes. We completed this as a fast follow to our <strong>September 17, 2025 <a class="" href="https://phasetwo.io/blog/soc-2-type-II-compliance/">SOC 2 Type II compliance milestone</a></strong>, reaching full ISO/IEC 27001 certification just over six months later as part of our commitment to building a mature, enterprise-ready security program.</p>
<p>Learn more at our Trust Center: <strong><a href="https://trust.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">trust.phasetwo.io</a></strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-is-isoiec-27001-certification">What is ISO/IEC 27001 certification?<a href="https://phasetwo.io/blog/iso-27001-certification/#what-is-isoiec-27001-certification" class="hash-link" aria-label="Direct link to What is ISO/IEC 27001 certification?" title="Direct link to What is ISO/IEC 27001 certification?" translate="no">​</a></h2>
<p>ISO/IEC 27001 is an internationally recognized standard for establishing, implementing, maintaining, and continually improving an <strong>Information Security Management System (ISMS)</strong>.</p>
<p>In practical terms, it provides a structured framework for identifying risk, applying appropriate controls, documenting security processes, and continuously improving how an organization protects systems and data. Full certification means this program has been evaluated against the standard rather than being described only as an internal compliance effort.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-this-matters">Why this matters<a href="https://phasetwo.io/blog/iso-27001-certification/#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters" translate="no">​</a></h2>
<ul>
<li class=""><strong>Structured security management:</strong> ISO/IEC 27001 formalizes how security risks are identified, assessed, treated, and reviewed over time.</li>
<li class=""><strong>Independent confidence:</strong> Certification against a globally recognized standard gives customers and prospects greater confidence in our security posture.</li>
<li class=""><strong>Faster vendor reviews:</strong> A stronger compliance foundation helps reduce friction during procurement, security questionnaires, and customer audits.</li>
<li class=""><strong>Continuous improvement:</strong> ISO/IEC 27001 is designed around ongoing review and maturation, not a one-time checkpoint.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="a-fast-follow-after-soc-2-type-ii">A Fast Follow After SOC 2 Type II<a href="https://phasetwo.io/blog/iso-27001-certification/#a-fast-follow-after-soc-2-type-ii" class="hash-link" aria-label="Direct link to A Fast Follow After SOC 2 Type II" title="Direct link to A Fast Follow After SOC 2 Type II" translate="no">​</a></h2>
<p>We take security and compliance very seriously at Phase Two. Achieving full ISO/IEC 27001 certification just over six months after our September 17, 2025 SOC 2 Type II compliance milestone was an intentional investment in operational maturity, not a box-checking exercise.</p>
<p>This fast follow reflects the pace at which we have continued to strengthen our internal security program, including risk management, policy development, operational controls, documented procedures, and accountability across the organization.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-this-means-for-customers">What this means for customers<a href="https://phasetwo.io/blog/iso-27001-certification/#what-this-means-for-customers" class="hash-link" aria-label="Direct link to What this means for customers" title="Direct link to What this means for customers" translate="no">​</a></h2>
<ul>
<li class=""><strong>Higher confidence in our processes:</strong> Our security program is built around a clear, repeatable framework for managing information security.</li>
<li class=""><strong>Better support for enterprise requirements:</strong> Customers with formal security review processes benefit from a stronger compliance posture and clearer documentation.</li>
<li class=""><strong>Ongoing resilience:</strong> A mature ISMS helps support secure delivery, reliable operations, and a disciplined response to evolving risks.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-next">What’s next<a href="https://phasetwo.io/blog/iso-27001-certification/#whats-next" class="hash-link" aria-label="Direct link to What’s next" title="Direct link to What’s next" translate="no">​</a></h2>
<p>Security and compliance are continuous commitments. We will keep investing in the controls, processes, and operational rigor required to support customers with demanding security and compliance expectations.</p>
<p>Phase Two’s <strong><a class="" href="https://phasetwo.io/hosting/">hosting</a></strong> product continues to support organizations across healthcare, security, retail, public sector, and more. Milestones like ISO/IEC 27001 certification help us meet the expectations of customers who need strong identity infrastructure backed by strong operational discipline.</p>
<p>Thank you for trusting Phase Two with your business. If you would like to learn more, please visit <strong><a href="https://trust.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">trust.phasetwo.io</a></strong> or email <strong><a href="mailto:sales@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">sales@phasetwo.io</a></strong>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>security</category>
            <category>hosting</category>
            <category>iso-27001</category>
        </item>
        <item>
            <title><![CDATA[Replacing Keycloak's Infinispan Caches with Redis/Valkey (Keycloak DevDay 2026)]]></title>
            <link>https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/</link>
            <guid>https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/</guid>
            <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical overview of our work to replace Keycloak distributed Infinispan caches with Redis/Valkey, including architecture, implementation patterns, and benchmark outcomes.]]></description>
            <content:encoded><![CDATA[<p>At Keycloak DevDay 2026, we shared our work on replacing Keycloak's distributed <a href="https://infinispan.org/" target="_blank" rel="noopener noreferrer" class="">Infinispan</a> caches with <a href="https://redis.io/" target="_blank" rel="noopener noreferrer" class="">Redis</a>/<a href="https://valkey.io/" target="_blank" rel="noopener noreferrer" class="">Valkey</a>.</p>
<p>For the full technical deep dive, we will release slides when the talk is published on Youtube.</p>
<p>This post focuses on the core technical content from the presentation and summarizes what we built, what we learned, and what comes next.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="watch-the-talk">Watch the talk<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#watch-the-talk" class="hash-link" aria-label="Direct link to Watch the talk" title="Direct link to Watch the talk" translate="no">​</a></h2>
<iframe width="560" height="315" src="https://www.youtube.com/embed/rHeOQejcCQI?si=9QvaQ1JvUBTb7E-k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-we-took-this-on">Why We Took This On<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#why-we-took-this-on" class="hash-link" aria-label="Direct link to Why We Took This On" title="Direct link to Why We Took This On" translate="no">​</a></h2>
<p>The recurring operator pain points were clear: restart complexity, JGroups/Infinispan operational friction, and upgrade risk in high-availability environments.</p>
<p>Our goals for the proof-of-concept were practical:</p>
<ol>
<li class="">Functional parity with Keycloak's distributed cache behavior.</li>
<li class="">At least 50% of embedded Infinispan performance.</li>
<li class="">Fast startup (sub-5s per pod) with reliable behavior.</li>
<li class="">Seamless upgrades and horizontal scaling without rebalance pain.</li>
<li class="">Extension-based implementation on current Keycloak, without forking.</li>
</ol>
<p>We also set explicit <strong>non-goals</strong>, including replacing local Infinispan caches, integrating/migrating persistent sessions, supporting every Redis topology, and solving multi-region concerns in v1.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-we-implemented-high-level">What We Implemented (High Level)<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#what-we-implemented-high-level" class="hash-link" aria-label="Direct link to What We Implemented (High Level)" title="Direct link to What We Implemented (High Level)" translate="no">​</a></h2>
<p>We implemented a new <a href="https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/storage/DatastoreProvider.html" target="_blank" rel="noopener noreferrer" class=""><code>DatastoreProvider</code></a>-based approach to replace distributed caches while leaving the rest of Keycloak behavior intact.</p>
<p>At a high level:</p>
<ol>
<li class="">Stored entities as Redis hashes (session and related objects).</li>
<li class="">Added secondary indexes with Redis sets for fast lookup paths (for example, user-to-sessions, client-to-sessions).</li>
<li class="">Built a changelog-based transaction implementation of <a href="https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/models/KeycloakTransaction.html" target="_blank" rel="noopener noreferrer" class=""><code>KeycloakTransaction</code></a> to batch and minimize writes at the appropriate stage of the Keycloak request lifecycle.</li>
<li class="">Used Redis <code>MULTI/EXEC</code> for batched commit behavior.</li>
<li class="">Implemented Lua-based CAS behavior for single-round-trip atomic updates.</li>
<li class="">Replaced the use of the replicated work cache, used for local cache invalidation, with a Redis PUBSUB-backed <code>ClusterProvider</code>.</li>
<li class="">Exposed Redis client and command metrics in Keycloak's Prometheus endpoint (<code>vendor_jedis_*</code>).</li>
</ol>
<p>For detailed APIs, object mapping rules, and transaction semantics, the slide deck has the full implementation flow and diagrams.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="notable-engineering-details">Notable Engineering Details<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#notable-engineering-details" class="hash-link" aria-label="Direct link to Notable Engineering Details" title="Direct link to Notable Engineering Details" translate="no">​</a></h2>
<p>A few areas mattered most in practice:</p>
<ol>
<li class=""><strong>Atomicity model:</strong> Lua CAS operations reduced contention and retry complexity versus <code>WATCH</code>-based optimistic workflows.</li>
<li class=""><strong>Expiration correctness:</strong> TTL handling required careful validation to avoid stale references (for example, refresh token edge cases).</li>
<li class=""><strong>Behavior parity:</strong> Reproducing nuanced Infinispan behavior (especially around offline session flows and invalidation semantics) required close reading of existing Keycloak internals.</li>
<li class=""><strong>Index discipline:</strong> Clean separation between hash storage and set-based secondary indexes kept lookups efficient and made invalidation predictable.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="benchmarks-and-outcomes">Benchmarks and Outcomes<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#benchmarks-and-outcomes" class="hash-link" aria-label="Direct link to Benchmarks and Outcomes" title="Direct link to Benchmarks and Outcomes" translate="no">​</a></h2>
<p>In our benchmark setup, the Redis implementation met and exceeded the original performance target. The result was parity or better across the tested flow, while also meeting the startup-speed and extension/no-fork constraints. We used the Keycloak provided <a href="https://github.com/keycloak/keycloak-benchmark" target="_blank" rel="noopener noreferrer" class="">keycloak-benchmark</a> project to execute performance tests against a 3 node cluster using Redis caching, and compared the results to a standard Keycloak setup on the same number of nodes, using embedded Infinispan.</p>
<p>This approach hit the proof-of-concept goals, with room for additional optimization and broader validation.</p>
<div><table class="table w-full border-collapse bg-white/10 text-sm"><thead class="bg-white/10"><tr><th class="px-4 py-3 text-left font-semibold text-white"><p>Benchmark</p></th><th class="px-4 py-3 text-right font-semibold text-white"><p>Infinispan (99th pct)</p></th><th class="px-4 py-3 text-right font-semibold text-white"><p>Redis (99th pct)</p></th><th class="px-4 py-3 text-right font-semibold text-white">Delta</th></tr></thead><tbody class="divide-y divide-white/15"><tr><td class="px-4 py-3 text-white">All Requests</td><td class="px-4 py-3 text-right text-gray-200">67</td><td class="px-4 py-3 text-right text-gray-200">47</td><td class="px-4 py-3 text-right"><span class="font-semibold text-green-400">29.9%</span></td></tr><tr><td class="px-4 py-3 text-white">Browser to Log In Endpoint</td><td class="px-4 py-3 text-right text-gray-200">71</td><td class="px-4 py-3 text-right text-gray-200">86</td><td class="px-4 py-3 text-right"><span class="font-semibold text-red-400">-21.1%</span></td></tr><tr><td class="px-4 py-3 text-white"><p>Browser posts correct credentials</p></td><td class="px-4 py-3 text-right text-gray-200">83</td><td class="px-4 py-3 text-right text-gray-200">57</td><td class="px-4 py-3 text-right"><span class="font-semibold text-green-400">31.3%</span></td></tr><tr><td class="px-4 py-3 text-white">Exchange Code</td><td class="px-4 py-3 text-right text-gray-200">22</td><td class="px-4 py-3 text-right text-gray-200">10</td><td class="px-4 py-3 text-right"><span class="font-semibold text-green-400">54.5%</span></td></tr><tr><td class="px-4 py-3 text-white">RefreshToken</td><td class="px-4 py-3 text-right text-gray-200">36</td><td class="px-4 py-3 text-right text-gray-200">12</td><td class="px-4 py-3 text-right"><span class="font-semibold text-green-400">66.7%</span></td></tr><tr><td class="px-4 py-3 text-white">Browser logout</td><td class="px-4 py-3 text-right text-gray-200">43</td><td class="px-4 py-3 text-right text-gray-200">9</td><td class="px-4 py-3 text-right"><span class="font-semibold text-green-400">79.1%</span></td></tr></tbody></table></div>
<p><em>Note: lower 99th percentile values are better. The "Browser to Log In Endpoint" row is the one regression shown in the original chart.</em></p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="benchmark-screenshots">Benchmark screenshots<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#benchmark-screenshots" class="hash-link" aria-label="Direct link to Benchmark screenshots" title="Direct link to Benchmark screenshots" translate="no">​</a></h3>
<p>Redis benchmark test results:</p>
<p><img decoding="async" loading="lazy" alt="Redis benchmark test results" src="https://phasetwo.io/assets/images/redis-test-results-cd826d293dfa9febc7ea3e989facc73b.png" width="2692" height="592" class="img_R7yg"></p>
<p>Infinispan benchmark test results:</p>
<p><img decoding="async" loading="lazy" alt="Infinispan benchmark test results" src="https://phasetwo.io/assets/images/infinispan-test-results-5af855541eda5217206c39ce9c04864d.png" width="2710" height="590" class="img_R7yg"></p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="timeline-and-context">Timeline and Context<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#timeline-and-context" class="hash-link" aria-label="Direct link to Timeline and Context" title="Direct link to Timeline and Context" translate="no">​</a></h2>
<p>This work started in <strong>September 2025</strong>.</p>
<p>A successful implementation would not have been possible without years of prior work in Keycloak internals, extension development, and production operations. Existing knowledge around map-store patterns, DatastoreProvider internals, transaction behavior, and real-world customer requirements was the foundation that made this tractable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-next">What's Next<a href="https://phasetwo.io/blog/keycloak-devday-2026-redis-valkey-caches/#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next" translate="no">​</a></h2>
<p>Near-term next steps include broader performance validation across workloads, clearer packaging/licensing for source and images, and additional multi-region validation/documentation.</p>
<p>We also plan to release a full image soon at <a href="https://quay.io/repository/phasetwo/keycloak-redis" target="_blank" rel="noopener noreferrer" class="">quay.io/repository/phasetwo/keycloak-redis</a>, so stay tuned for more.</p>
<p>If you want all the implementation specifics, design tradeoffs, and diagrams, please stay tuned for the release of the slides and videos of our Keycloak DevDay presentation.</p>
<hr>
<p>Want to go deeper?</p>
<ol>
<li class="">Reach out to our team: <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a></li>
<li class="">Try our hosting and extensions: <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a></li>
<li class="">Contact us for additional implementation details and architecture guidance: <a href="https://phasetwo.io/contact" target="_blank" rel="noopener noreferrer" class="">https://phasetwo.io/contact</a></li>
</ol>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>redis</category>
            <category>valkey</category>
            <category>caching</category>
            <category>performance</category>
            <category>extensions</category>
        </item>
        <item>
            <title><![CDATA[Cluster Observability and Logs]]></title>
            <link>https://phasetwo.io/blog/cluster-observability-and-logs/</link>
            <guid>https://phasetwo.io/blog/cluster-observability-and-logs/</guid>
            <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two now includes built-in observability for dedicated clusters, starting with log downloads via the self-service dashboard.]]></description>
            <content:encoded><![CDATA[<p>We’re excited to announce built-in observability for dedicated clusters, starting with <strong>log downloads</strong> in the Phase Two dashboard.</p>
<p>Access to logs is essential for monitoring health, diagnosing incidents, and understanding performance. With this initial release, you can filter logs by time range and download them as a zip file for offline analysis. This helps teams quickly pinpoint issues and keep their services running smoothly.</p>
<p>Next up: <strong>live log streaming</strong> and <strong>exports to external systems</strong>, giving you more flexibility for centralized log management and analysis.</p>
<p>Learn more in the <a href="https://phasetwo.io/docs/self-service/observability/" target="_blank" rel="noopener noreferrer" class="">Observability and Logs documentation</a>.</p>
<p>Ready to try it? Log in to the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and view logs for your cluster. This feature is included with all <strong>dedicated cluster plans</strong>. Questions? Reach us at <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
            <category>observability</category>
            <category>logs</category>
        </item>
        <item>
            <title><![CDATA[Cockroach Labs features Phase Two's Managed Keycloak Hosting]]></title>
            <link>https://phasetwo.io/blog/cockroach-labs-features-phasetwo-managed-keycloak-hosting/</link>
            <guid>https://phasetwo.io/blog/cockroach-labs-features-phasetwo-managed-keycloak-hosting/</guid>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cockroach Labs highlights Phase Two's managed Keycloak hosting platform in their latest blog post.]]></description>
            <content:encoded><![CDATA[<p>We’re pleased to share that Cockroach Labs has published a new blog post featuring Phase Two and our managed Keycloak hosting platform.</p>
<p>In <a href="https://www.cockroachlabs.com/blog/deploying-keycloak-on-cockroachdb-with-phase-two/" target="_blank" rel="noopener noreferrer" class=""><em>Deploying Keycloak on CockroachDB with Phase Two: A Complete Guide</em></a>, Cockroach Labs walks through how Phase Two and CockroachDB work together to provide a strong foundation for secure, multi-tenant identity at scale. The post outlines why strong consistency, high availability, and horizontal scalability are critical requirements for modern SaaS identity systems—and how combining managed Keycloak with CockroachDB meets those needs in production environments.</p>
<p>The article also includes an overview of Phase Two’s managed Keycloak offering, highlighting how teams can offload operational complexity such as infrastructure management, upgrades, backups, and high-availability configuration. By pairing this with CockroachDB’s globally distributed SQL database, organizations can deploy enterprise-grade authentication and SSO while maintaining strong consistency and predictable performance.</p>
<p>We appreciate Cockroach Labs’ collaboration and the opportunity to showcase how this complementary stack helps SaaS teams move faster without compromising on reliability or security.</p>
<p>You can read the full post on the Cockroach Labs blog here:
<a href="https://www.cockroachlabs.com/blog/deploying-keycloak-on-cockroachdb-with-phase-two/" target="_blank" rel="noopener noreferrer" class=""><strong>Deploying Keycloak on CockroachDB with Phase Two: A Complete Guide</strong></a></p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
            <category>cockroachdb</category>
        </item>
        <item>
            <title><![CDATA[Security Capabilities Now Available for Keycloak Clusters]]></title>
            <link>https://phasetwo.io/blog/dedicated-clusters-security-capabilities/</link>
            <guid>https://phasetwo.io/blog/dedicated-clusters-security-capabilities/</guid>
            <pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Enhance the security of your Keycloak clusters with new features available on the Phase Two Dash.]]></description>
            <content:encoded><![CDATA[<p>At Phase Two, we are committed to providing our customers with the most secure and reliable managed Keycloak hosting platform. As part of this commitment, we are excited to announce the release of new security capabilities for Keycloak clusters available through the Phase Two Dash.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="new-security-features">New Security Features<a href="https://phasetwo.io/blog/dedicated-clusters-security-capabilities/#new-security-features" class="hash-link" aria-label="Direct link to New Security Features" title="Direct link to New Security Features" translate="no">​</a></h2>
<p>We have introduced the first of several new security features that can be easily configured through the Phase Two Dash. These features are designed to enhance the security posture of your Keycloak clusters and provide you with greater control over your authentication environment.</p>
<p>The initial set of security features includes the ability to configure <strong>IP level access</strong> to your Keycloak clusters. There are three types of IP restrictions you can apply:</p>
<ol>
<li class=""><strong>Admin Paths</strong>: Restrict access to the administrative interfaces of your Keycloak instance (e.g., /admin, /auth/admin) to specific IP addresses or ranges. This ensures that only authorized personnel can access sensitive admin functions.</li>
<li class=""><strong>Public Allowed Paths</strong>: Specify IP addresses that are allowed to access public endpoints (e.g., /token). This is useful for limiting access to known clients while blocking all other traffic. Particularly useful for development and staging environments.</li>
<li class=""><strong>Public Blocked Paths</strong>: Block specific IP addresses from accessing public endpoints (e.g., /token). This allows you to prevent known malicious IPs from interacting with your Keycloak instance without affecting legitimate traffic. Or to just block abuse to your application for any reason.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-to-configure-security-features">How to Configure Security Features<a href="https://phasetwo.io/blog/dedicated-clusters-security-capabilities/#how-to-configure-security-features" class="hash-link" aria-label="Direct link to How to Configure Security Features" title="Direct link to How to Configure Security Features" translate="no">​</a></h2>
<p>Visit your cluster's page &gt; Config tab &gt; Restrictions to manage IP restrictions for your Keycloak cluster. You can easily add or remove IP addresses or CIDR ranges for each type of restriction.</p>
<p>Learn more in the <a href="https://phasetwo.io/docs/self-service/restrictions/" target="_blank" rel="noopener noreferrer" class="">Cluster Restrictions documentation</a>.</p>
<hr>
<p>Want to learn more or have feedback? Log in to the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and try adding your IP restriction now. This feature is included with all <strong>dedicated cluster plans</strong>. For any questions or concerns, please reach out to <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[Configure Environment Variables for Dedicated Keycloak Clusters]]></title>
            <link>https://phasetwo.io/blog/dedicated-clusters-environment-variables/</link>
            <guid>https://phasetwo.io/blog/dedicated-clusters-environment-variables/</guid>
            <pubDate>Mon, 29 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[How to set environment variables for dedicated Keycloak clusters on the Phase Two Dash]]></description>
            <content:encoded><![CDATA[<p>Phase Two has been storming ahead with our managed Keycloak hosting platform, <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">dash.phasetwo.io</a>. As part of our commitment to providing flexible and powerful hosting solutions, we are excited to announce that users can now set environment variables for their dedicated Keycloak clusters directly through the Phase Two Dash.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-environment-variables">Why Environment Variables?<a href="https://phasetwo.io/blog/dedicated-clusters-environment-variables/#why-environment-variables" class="hash-link" aria-label="Direct link to Why Environment Variables?" title="Direct link to Why Environment Variables?" translate="no">​</a></h2>
<p>Environment variables are a crucial aspect of configuring and managing applications. They allow you to customize the behavior of your Keycloak instances without modifying the underlying code or configuration files. This is especially important for managing sensitive information such as API keys and other configuration settings. For customers building custom Keycloak extensions or integrating with third-party services, the ability to set environment variables is essential.</p>
<p>With this new feature, you can easily manage your Keycloak cluster's environment variables through a user-friendly interface, streamlining the deployment and configuration process. Updates are applied seamlessly, requiring no downtime for your authentication services.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-to-set-environment-variables">How to Set Environment Variables<a href="https://phasetwo.io/blog/dedicated-clusters-environment-variables/#how-to-set-environment-variables" class="hash-link" aria-label="Direct link to How to Set Environment Variables" title="Direct link to How to Set Environment Variables" translate="no">​</a></h2>
<p>Within your cluster, visit your cluster's page &gt; Config tab &gt; Keycloak features. From there, you can add new environment variables or remove existing ones. You have the option to set variables as plaintext or as secrets, ensuring that sensitive information remains secure.</p>
<p>Learn more in the <a href="https://phasetwo.io/docs/self-service/environment-variables/" target="_blank" rel="noopener noreferrer" class="">Environment Variables documentation</a>.</p>
<hr>
<p>Want to learn more or have feedback? Log in to the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and try adding your first environment variable. This feature is included with all <strong>dedicated cluster plans</strong>. For any questions or concerns, please reach out to <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
        </item>
        <item>
            <title><![CDATA[Auth.it featured on Keycloak Friday Chat]]></title>
            <link>https://phasetwo.io/blog/authit-on-kfc/</link>
            <guid>https://phasetwo.io/blog/authit-on-kfc/</guid>
            <pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two's new hosting platform, Auth.it, was featured on Mr. Keycloak's livestream]]></description>
            <content:encoded><![CDATA[<p>Phase Two has recently <a href="https://auth.it/blog/welcome" target="_blank" rel="noopener noreferrer" class="">launched</a> <a href="https://auth.it/" target="_blank" rel="noopener noreferrer" class="">Auth.it</a>, a modern authentication platform built for developers who want the power of Keycloak with the simplicity, polish, and developer experience of modern identity providers like WorkOS, Stytch, and Clerk — all at a fraction of the cost.</p>
<p>Last Friday, Niko Köbler (aka "Mr. Keycloak"), invited us to demonstrate Auth.it and explain how we built it on his livestream, <a href="https://www.n-k.de/2025/12/kfc-keycloak-friday-chat-xmas-edition-authit-phase-two.html" target="_blank" rel="noopener noreferrer" class="">Keycloak Friday Chat</a>. If you're interested in an overview of the new platform, and would like to know the details of how it was implemented as a set of Keycloak extensions, please <a href="https://www.youtube.com/watch?v=a2thzEs35ws" target="_blank" rel="noopener noreferrer" class="">watch the recording of the livestream</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
            <category>authit</category>
        </item>
        <item>
            <title><![CDATA[Run the Keycloak Admin UI Locally (with Phase Two Extensions)]]></title>
            <link>https://phasetwo.io/blog/run-admin-ui/</link>
            <guid>https://phasetwo.io/blog/run-admin-ui/</guid>
            <pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Set up a fast local development environment for the Keycloak Admin UI and point it at a custom Keycloak image—perfect for iterating on extensions like Keycloak Organizations.]]></description>
            <content:encoded><![CDATA[<p>Developing custom additions to the Keycloak Admin UI can be fiddly and slow. At Phase Two we maintain several <a href="https://github.com/p2-inc#our-extensions-" target="_blank" rel="noopener noreferrer" class="">popular community extensions</a> that must track frequent Keycloak releases. Below is the approach we use to develop and verify Admin UI changes quickly against a running Keycloak image that includes our extensions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="tldr">TL;DR<a href="https://phasetwo.io/blog/run-admin-ui/#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<ol>
<li class="">Clone our Keycloak fork and check out the matching Admin UI branch (e.g., <code>26.3.0_orgs_adminui</code>).</li>
<li class="">Replace the default server script with our custom one (details below).</li>
<li class="">Start a Phase Two Keycloak container with the Admin UI in dev mode:</li>
</ol>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pnpm start --admin-dev --phasetwo --tag=latest</span><br></div></code></pre></div></div>
<ol start="4">
<li class="">In a second terminal, run the Admin UI dev server:</li>
</ol>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pnpm i &amp;&amp; pnpm dev</span><br></div></code></pre></div></div>
<p>Open <a href="http://localhost:8080/" target="_blank" rel="noopener noreferrer" class="">http://localhost:8080</a>. You’ll get auto-refresh and modern DX while the backend runs in Docker.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-this-matters">Why this matters<a href="https://phasetwo.io/blog/run-admin-ui/#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters" translate="no">​</a></h2>
<p>The Admin UI is the control center for Keycloak. Customizing it is powerful, but it’s tightly coupled to the core codebase and evolves quickly. Running the UI in dev mode against <strong>the exact server image</strong> you ship (e.g., Phase Two’s extended image) lets you:</p>
<ul>
<li class="">Catch breaking changes early when Keycloak releases.</li>
<li class="">Iterate on UI changes instantly (no re-building JARs between edits).</li>
<li class="">Validate that your extensions render and behave correctly with real data.</li>
</ul>
<p>Our <a href="https://github.com/p2-inc/keycloak-orgs" target="_blank" rel="noopener noreferrer" class="">Keycloak Organizations</a> extension, for example, adds new sections to the Admin Console. Each release we re-verify and adjust UI components; this workflow makes that practical.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-the-stock-setup-works-and-why-we-tweak-it">How the stock setup works (and why we tweak it)<a href="https://phasetwo.io/blog/run-admin-ui/#how-the-stock-setup-works-and-why-we-tweak-it" class="hash-link" aria-label="Direct link to How the stock setup works (and why we tweak it)" title="Direct link to How the stock setup works (and why we tweak it)" translate="no">​</a></h2>
<p>Keycloak’s Admin UI is now a library inside the main repo. You can run it locally against a server, but the stock start script pulls the <strong>nightly</strong> image—great for core dev, not ideal when you need to test against a specific custom image.</p>
<p>To fix that, we use a small custom start script that runs a <strong>Phase Two Keycloak container</strong> (or any image—see below) and wires the Admin UI dev server to it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="prerequisites">Prerequisites<a href="https://phasetwo.io/blog/run-admin-ui/#prerequisites" class="hash-link" aria-label="Direct link to Prerequisites" title="Direct link to Prerequisites" translate="no">​</a></h2>
<ul>
<li class="">Node 20+</li>
<li class=""><code>pnpm</code> 9+</li>
<li class="">Docker Desktop / Docker 24+</li>
<li class="">Ports <strong>8080</strong> (Keycloak) and <strong>5174</strong> (Admin UI) free</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="step-by-step">Step-by-step<a href="https://phasetwo.io/blog/run-admin-ui/#step-by-step" class="hash-link" aria-label="Direct link to Step-by-step" title="Direct link to Step-by-step" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="1-get-the-right-code">1) Get the right code<a href="https://phasetwo.io/blog/run-admin-ui/#1-get-the-right-code" class="hash-link" aria-label="Direct link to 1) Get the right code" title="Direct link to 1) Get the right code" translate="no">​</a></h3>
<ul>
<li class="">Clone our fork and check out the branch that matches your Keycloak target (we don’t publish a branch for every upstream release):</li>
</ul>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain"># example</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">git clone https://github.com/p2-inc/keycloak.git</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd keycloak</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">git checkout 26.3.0_orgs_adminui</span><br></div></code></pre></div></div>
<ul>
<li class="">Admin UI lives under <code>js/apps/admin-ui/</code>. The embedded server project lives under <code>js/apps/keycloak-server/</code>.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="2-swap-in-the-custom-start-script">2) Swap in the custom start script<a href="https://phasetwo.io/blog/run-admin-ui/#2-swap-in-the-custom-start-script" class="hash-link" aria-label="Direct link to 2) Swap in the custom start script" title="Direct link to 2) Swap in the custom start script" translate="no">​</a></h3>
<p>Copy the contents of <code>js/apps/admin-ui/src/phaseII/start-server-custom.js</code> into <code>js/apps/keycloak-server/scripts/start-server.js</code> <strong>replacing its contents</strong>.</p>
<blockquote>
<p>The custom script adds flags like <code>--phasetwo</code> and <code>--tag</code> and starts <code>quay.io/phasetwo/phasetwo-keycloak:&lt;tag&gt;</code> via Docker with the right dev env vars.</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="3-if-needed-align-the-theme-output-name">3) (If needed) Align the theme output name<a href="https://phasetwo.io/blog/run-admin-ui/#3-if-needed-align-the-theme-output-name" class="hash-link" aria-label="Direct link to 3) (If needed) Align the theme output name" title="Direct link to 3) (If needed) Align the theme output name" translate="no">​</a></h3>
<p>Our Admin UI build emits a theme named <code>phasetwo.v2</code>. If your environment expects <code>keycloak.v2</code>, either:</p>
<ul>
<li class="">Duplicate the folder <code>theme/phasetwo.v2</code> and name it <code>keycloak.v2</code>, <strong>or</strong></li>
<li class="">Update <code>vite.config.ts</code> to output to <code>phasetwo.v2</code> and keep the server pointed at that name.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="4-start-the-server-and-the-ui">4) Start the server and the UI<a href="https://phasetwo.io/blog/run-admin-ui/#4-start-the-server-and-the-ui" class="hash-link" aria-label="Direct link to 4) Start the server and the UI" title="Direct link to 4) Start the server and the UI" translate="no">​</a></h3>
<ul>
<li class="">Start the server container + wire the UI dev endpoints:</li>
</ul>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd js/apps/keycloak-server</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pnpm start --admin-dev --phasetwo --tag=latest</span><br></div></code></pre></div></div>
<ul>
<li class="">In another terminal, start the Admin UI dev server:</li>
</ul>
<div class="language-bash codeBlockContainer_Z3mN theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_pizs"><pre tabindex="0" class="prism-code language-bash codeBlock_iQew thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_ce1h"><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd ../admin-ui</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pnpm i</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pnpm dev</span><br></div></code></pre></div></div>
<p>Now open <a href="http://localhost:8080/" target="_blank" rel="noopener noreferrer" class="">http://localhost:8080</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="script-flags">Script flags<a href="https://phasetwo.io/blog/run-admin-ui/#script-flags" class="hash-link" aria-label="Direct link to Script flags" title="Direct link to Script flags" translate="no">​</a></h2>
<table><thead><tr><th>Flag</th><th>Type</th><th>What it does</th></tr></thead><tbody><tr><td><code>--local</code></td><td>boolean</td><td>Use a local Quarkus dist (<code>keycloak-999.0.0-SNAPSHOT.tar.gz</code>) instead of Docker.</td></tr><tr><td><code>--account-dev</code></td><td>boolean</td><td>Points Account app to <code>http://localhost:5173</code> for dev.</td></tr><tr><td><code>--admin-dev</code></td><td>boolean</td><td>Points Admin app to <code>http://localhost:5174</code> for dev (use this tutorial).</td></tr><tr><td><code>--phasetwo</code></td><td>boolean</td><td>Run <code>quay.io/phasetwo/phasetwo-keycloak</code> in Docker with dev-friendly flags.</td></tr><tr><td><code>--tag=&lt;tag&gt;</code></td><td>string</td><td>Image tag to run (defaults to <code>latest</code>).</td></tr></tbody></table>
<blockquote>
<p>Under the hood we enable features required by our extensions: <code>login:v2, account:v3, admin-fine-grained-authz:v2, transient-users, organization, oid4vc-vci, declarative-ui, quick-theme</code>.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="troubleshooting">Troubleshooting<a href="https://phasetwo.io/blog/run-admin-ui/#troubleshooting" class="hash-link" aria-label="Direct link to Troubleshooting" title="Direct link to Troubleshooting" translate="no">​</a></h2>
<ul>
<li class=""><strong>Port 8080 already in use</strong> – Stop other Keycloak/containers or change the published port.</li>
<li class=""><strong>White screen / 404s in the Admin UI</strong> – Make sure <code>--admin-dev</code> is set and the dev server is running on <code>5174</code>.</li>
<li class=""><strong>Missing strings/components</strong> – Verify you’re on the matching Admin UI branch for your Keycloak version.</li>
<li class=""><strong>Theme not loading</strong> – Ensure the output theme name (<code>phasetwo.v2</code> vs <code>keycloak.v2</code>) matches what the server expects.</li>
<li class=""><strong>Cannot pull image</strong> – Confirm Docker can access <code>quay.io</code> and that the tag exists.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="conclusion">Conclusion<a href="https://phasetwo.io/blog/run-admin-ui/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>This workflow keeps the Admin UI dev loop fast while staying faithful to the image you deploy. Although we demonstrate with Phase Two’s image, the same pattern works with any Keycloak distribution. If this saved you time, drop us a star ⭐️ on <a href="https://github.com/p2-inc" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
<hr>
<p>Want to learn more? Log in to the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and try uploading your first resource today. This feature is included with all <strong>dedicated cluster plans</strong>. For questions, reach out to <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>open_source</category>
            <category>keycloak</category>
            <category>admin-ui</category>
            <category>docker</category>
            <category>extensions</category>
            <category>dev-environment</category>
            <category>pnpm</category>
        </item>
        <item>
            <title><![CDATA[Phase Two Releases Self Service Resource Management]]></title>
            <link>https://phasetwo.io/blog/self-service-resource-management/</link>
            <guid>https://phasetwo.io/blog/self-service-resource-management/</guid>
            <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two now offers self-service resource management for dedicated clusters, allowing customers to easily add and remove resources through our dashboard.]]></description>
            <content:encoded><![CDATA[<p>We’re excited to announce the release of <strong>Self-Service Resource Management</strong> in the Phase Two Dash. This new capability empowers customers running <strong>dedicated Keycloak clusters</strong> to directly upload and manage their own resources—such as custom themes and extensions—without waiting for Phase Two intervention.</p>
<div class="theme-admonition theme-admonition-info admonition_Y66Y alert alert--info"><div class="admonitionHeading_Feou"><span class="admonitionIcon_RBEH"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_oUgd"><p><strong>Available for Dedicated Clusters Only</strong><br>
<!-- -->Self-Service Resource Management is an exclusive feature for Phase Two customers with dedicated Keycloak clusters. Shared or multi-tenant environments do not include this capability. Upgrade to a dedicated cluster plan to take advantage of this feature.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-this-matters">Why this matters<a href="https://phasetwo.io/blog/self-service-resource-management/#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters" translate="no">​</a></h2>
<p>Traditionally, managing custom Keycloak resources in Kubernetes is complex, time-consuming, and error-prone. It often requires deep knowledge of deployment pipelines, containerization, and manual version tracking. With Self-Service Resource Management, Phase Two eliminates those hurdles by giving you a streamlined interface in the Dash to manage your resources safely and reliably.</p>
<p>Because this feature is only available for <strong>dedicated clusters</strong>, customers benefit from the highest level of flexibility, security, and control over their Keycloak environments.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="key-features">Key Features<a href="https://phasetwo.io/blog/self-service-resource-management/#key-features" class="hash-link" aria-label="Direct link to Key Features" title="Direct link to Key Features" translate="no">​</a></h2>
<ul>
<li class="">
<p><strong>Direct uploads</strong><br>
<!-- -->Upload <code>.jar</code> files (themes or extensions) directly through the dashboard, tied to your dedicated cluster.</p>
</li>
<li class="">
<p><strong>Versioning support</strong><br>
<!-- -->Create and manage resource versions. You can pin versions to specific Keycloak builds, which is especially useful for testing compatibility with future major Keycloak releases.</p>
</li>
<li class="">
<p><strong>Cluster-safe updates</strong><br>
<!-- -->Phase Two automatically manages cluster restarts to ensure resources are applied without risking downtime or instability.</p>
</li>
<li class="">
<p><strong>Faster release cycles</strong><br>
<!-- -->Reduce dependency on Phase Two deployment reviews. Customers can move quickly, automate feature rollouts, and test updates on their own timeline.</p>
</li>
<li class="">
<p><strong>Simplified workflows</strong><br>
<!-- -->Avoid the overhead of Kubernetes resource management, CI/CD pipeline adjustments, and manual rollbacks. Resource management is as simple as point-and-click.</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-this-means-for-customers">What this means for customers<a href="https://phasetwo.io/blog/self-service-resource-management/#what-this-means-for-customers" class="hash-link" aria-label="Direct link to What this means for customers" title="Direct link to What this means for customers" translate="no">​</a></h2>
<p>This feature brings enterprise-grade flexibility and speed to teams that rely on custom Keycloak deployments. Whether you’re testing a new extension for an upcoming Keycloak version or rolling out an updated theme across environments, you can now do it in minutes—not days. And with Phase Two’s guardrails in place, you still benefit from the reliability and security of our managed hosting platform.</p>
<hr>
<p>Want to learn more? Log in to the <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two Dash</a> and try uploading your first resource today. This feature is included with all <strong>dedicated cluster plans</strong>. For any questions or concerns, please reach out to <a href="mailto:support@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">support@phasetwo.io</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>hosting</category>
            <category>self-service</category>
            <category>resources</category>
            <category>keycloak</category>
            <category>extensions</category>
            <category>themes</category>
            <category>dedicated-clusters</category>
        </item>
        <item>
            <title><![CDATA[Phase Two Achieves SOC 2 Type II Compliance]]></title>
            <link>https://phasetwo.io/blog/soc-2-type-II-compliance/</link>
            <guid>https://phasetwo.io/blog/soc-2-type-II-compliance/</guid>
            <pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two has successfully completed a SOC 2 Type II audit, validating that our security and availability controls operated effectively over the audit period.]]></description>
            <content:encoded><![CDATA[<p>Phase Two is excited to share that we have successfully completed a <strong>SOC 2 Type II</strong> audit. This independently validates that our security and availability controls were not just designed well, but <strong>operated effectively over time</strong>.</p>
<p>Learn more and request report access at our Trust Center: <strong><a href="https://trust.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">trust.phasetwo.io</a></strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-is-soc-2-type-ii">What is SOC 2 Type II?<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#what-is-soc-2-type-ii" class="hash-link" aria-label="Direct link to What is SOC 2 Type II?" title="Direct link to What is SOC 2 Type II?" translate="no">​</a></h2>
<p>SOC 2 (System and Organization Controls 2) is an attestation standard for service organizations covering five Trust Services Criteria: <strong>security, availability, processing integrity, confidentiality, and privacy</strong>. A <strong>Type II</strong> report evaluates the <strong>operating effectiveness</strong> of controls <strong>across an audit period</strong>, rather than at a single point in time (Type I).</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-this-matters-to-customers">Why this matters to customers<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#why-this-matters-to-customers" class="hash-link" aria-label="Direct link to Why this matters to customers" title="Direct link to Why this matters to customers" translate="no">​</a></h2>
<ul>
<li class=""><strong>Operational assurance:</strong> Independent attestation that access control, change management, monitoring, incident response, and data protection controls <strong>worked throughout the evaluation period</strong>.</li>
<li class=""><strong>Faster vendor reviews:</strong> A current SOC 2 Type II report reduces back‑and‑forth during security assessments and procurement.</li>
<li class=""><strong>Lower risk, higher resilience:</strong> Tested controls help prevent incidents and improve recovery, supporting higher availability and reliability.</li>
<li class=""><strong>Transparent accountability:</strong> The auditor’s report provides objective evidence of our security posture and ongoing commitments.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="how-this-validates-phase-twos-approach">How this validates Phase Two’s approach<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#how-this-validates-phase-twos-approach" class="hash-link" aria-label="Direct link to How this validates Phase Two’s approach" title="Direct link to How this validates Phase Two’s approach" translate="no">​</a></h2>
<p>Our platform and operations are engineered for <strong>secure‑by‑default deployments</strong>, <strong>repeatable releases and change control</strong>, <strong>continuous monitoring</strong>, and <strong>documented incident response</strong>. The SOC 2 Type II result demonstrates these controls function in production—not just on paper.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-covered-in-the-report">What’s covered in the report<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#whats-covered-in-the-report" class="hash-link" aria-label="Direct link to What’s covered in the report" title="Direct link to What’s covered in the report" translate="no">​</a></h2>
<p>While each report is tailored to the service and period, customers can expect details on:</p>
<ul>
<li class="">In‑scope systems and boundaries</li>
<li class="">Control objectives and mapped Trust Services Criteria (with tests of operating effectiveness)</li>
<li class="">Auditor’s testing procedures and results</li>
<li class="">Complementary user entity controls (CUECs) relevant to your environment</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="accessing-the-report">Accessing the report<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#accessing-the-report" class="hash-link" aria-label="Direct link to Accessing the report" title="Direct link to Accessing the report" translate="no">​</a></h2>
<p>Phase Two makes the SOC 2 Type II report available to customers and qualified prospects <strong>under NDA</strong> via our Trust Center. If you need access for a vendor review, please submit a request at <strong><a href="https://trust.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">trust.phasetwo.io</a></strong> or email <strong><a href="mailto:sales@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">sales@phasetwo.io</a></strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-next">What’s next<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#whats-next" class="hash-link" aria-label="Direct link to What’s next" title="Direct link to What’s next" translate="no">​</a></h2>
<p>Compliance is continuous. We’ll keep strengthening automation, observability, and control effectiveness, and maintain ongoing attestation to reflect our customers’ evolving needs and the changing threat landscape.</p>
<p>We are actively pursuing ISO 27001 certification to further demonstrate our commitment to information security management.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-this-means-for-you">What this means for you<a href="https://phasetwo.io/blog/soc-2-type-II-compliance/#what-this-means-for-you" class="hash-link" aria-label="Direct link to What this means for you" title="Direct link to What this means for you" translate="no">​</a></h2>
<ul>
<li class=""><strong>Stronger trust:</strong> Independent validation you can include in your own compliance artifacts.</li>
<li class=""><strong>Faster assessments:</strong> Streamlined security questionnaires and procurement cycles.</li>
<li class=""><strong>Operational confidence:</strong> Controls that demonstrably reduce operational risk and improve incident response.</li>
</ul>
<p>Phase Two’s <strong><a class="" href="https://phasetwo.io/hosting/">hosting</a></strong> product supports organizations across healthcare, CDN and security, retail, and more—delivering predictable, enterprise‑grade identity and access management.</p>
<blockquote>
<p><strong>Need the report or have questions?</strong> Contact <strong><a href="mailto:sales@phasetwo.io" target="_blank" rel="noopener noreferrer" class="">sales@phasetwo.io</a></strong> or visit <strong><a href="https://trust.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">trust.phasetwo.io</a></strong>.</p>
</blockquote>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>security</category>
            <category>hosting</category>
            <category>soc-2</category>
        </item>
        <item>
            <title><![CDATA[Custom domains on Phase Two Paid Plans]]></title>
            <link>https://phasetwo.io/blog/custom-domains/</link>
            <guid>https://phasetwo.io/blog/custom-domains/</guid>
            <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Phase Two is happy to announce that custom domains can now be configured via self-serve right in the Dashboard. This feature is only available on paid plans.]]></description>
            <content:encoded><![CDATA[<p>Phase Two is happy to announce that custom domains can now be configured via self-serve right in the <a href="https://dash.phasetwo.com/" target="_blank" rel="noopener noreferrer" class="">Dashboard</a>. This feature is only available on paid plans.</p>
<p>Custom domains are a key piece of branding and trust for your users. By using your own domain, you can ensure that your users see a familiar URL when interacting with your Keycloak instance.</p>
<p>Read more about how to set up custom domains in our <a class="" href="https://phasetwo.io/docs/self-service/custom-domains/">Custom Domains documentation</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="custom-domains-overview">Custom Domains Overview<a href="https://phasetwo.io/blog/custom-domains/#custom-domains-overview" class="hash-link" aria-label="Direct link to Custom Domains Overview" title="Direct link to Custom Domains Overview" translate="no">​</a></h2>
<p>Up until this point, custom domains were offered, but had to be setup and managed by Phase Two support. Now, you can configure and manage your own custom domains directly from the Dashboard. This allows for multiple custom domains to be used as required.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-use-custom-domains">Why Use Custom Domains?<a href="https://phasetwo.io/blog/custom-domains/#why-use-custom-domains" class="hash-link" aria-label="Direct link to Why Use Custom Domains?" title="Direct link to Why Use Custom Domains?" translate="no">​</a></h2>
<p>Using a custom domain for your Keycloak instance provides several benefits:</p>
<ol>
<li class=""><strong>Branding</strong>: A custom domain allows you to maintain consistent branding across your applications and services. This helps reinforce your brand identity and provides a seamless experience for your users.</li>
<li class=""><strong>Trust</strong>: Users are more likely to trust a familiar domain. By using your
own domain, you can increase user confidence and reduce the likelihood of phishing attacks.</li>
<li class=""><strong>SSL/TLS</strong>: Phase Two provides free SSL/TLS certificates for your custom domains, ensuring that all communications are secure and encrypted.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="whats-next">What's next?<a href="https://phasetwo.io/blog/custom-domains/#whats-next" class="hash-link" aria-label="Direct link to What's next?" title="Direct link to What's next?" translate="no">​</a></h2>
<p>Phase Two has many exciting features planned for the future. Stay tuned for more updates and improvements to our platform.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>domains</category>
            <category>ssl</category>
            <category>certificates</category>
            <category>custom_domains</category>
        </item>
        <item>
            <title><![CDATA[Shadcn Keycloak Theme Starter with Tailwind CSS]]></title>
            <link>https://phasetwo.io/blog/shadcn-keycloak-theme/</link>
            <guid>https://phasetwo.io/blog/shadcn-keycloak-theme/</guid>
            <pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Get the theme: Shadcn Keycloak Theme Starter]]></description>
            <content:encoded><![CDATA[<p>Get the theme: <a href="https://github.com/p2-inc/keycloakify-starter-shadcn" target="_blank" rel="noopener noreferrer" class="">Shadcn Keycloak Theme Starter</a></p>
<p>A <a href="https://ui.shadcn.com/" target="_blank" rel="noopener noreferrer" class="">shadcn</a> theme starter for Keycloak using <a href="https://www.tailwindcss.com/" target="_blank" rel="noopener noreferrer" class="">Tailwind CSS</a> and <a href="https://www.radix-ui.com/" target="_blank" rel="noopener noreferrer" class="">Radix UI</a> components. This starter provides a foundation for building a custom, component-based Keycloak theme.</p>
<p>Phase Two are <a class="" href="https://phasetwo.io/blog/phasetwo-keycloakify-partnership/">sponsors</a> of <a href="https://www.keycloakify.dev/" target="_blank" rel="noopener noreferrer" class="">Keycloakify</a> as we are deeply convinced by this project's value.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="the-shadcn-keycloak-theme-starter">The Shadcn Keycloak Theme Starter<a href="https://phasetwo.io/blog/shadcn-keycloak-theme/#the-shadcn-keycloak-theme-starter" class="hash-link" aria-label="Direct link to The Shadcn Keycloak Theme Starter" title="Direct link to The Shadcn Keycloak Theme Starter" translate="no">​</a></h2>
<p>We chose to use <a href="https://ui.shadcn.com/" target="_blank" rel="noopener noreferrer" class="">shadcn</a> as the component library for this starter because it provides a set of modern, accessible, and customizable components that can be easily integrated into Keycloakify themes. shadcn is built on top of <a href="https://tailwindcss.com/" target="_blank" rel="noopener noreferrer" class="">Tailwind CSS</a> and <a href="https://www.radix-ui.com/" target="_blank" rel="noopener noreferrer" class="">Radix UI</a>, which makes it a great fit for building custom Keycloak themes.</p>
<p>The starter provides a basic structure for building Keycloak themes using shadcn components, including:</p>
<ul>
<li class=""><strong>Initial Setup</strong>: A basic Keycloakify project setup with shadcn and Tailwind CSS configured.</li>
<li class=""><strong>Login Page</strong>: A customized login page using shadcn components.</li>
<li class=""><strong>Baseline Components</strong>: A set of reusable shadcn components that can be used throughout the theme.</li>
<li class=""><strong>Baseline Ejections</strong>: Minimum set of pages ejected to get you started with customizing the login.</li>
</ul>
<p>Since shadcn ejects components into the project, you can customize them (or replace) as needed. This makes it quick and easy to introduce styles or components.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="options-for-customizing-keycloak-ui">Options for Customizing Keycloak UI<a href="https://phasetwo.io/blog/shadcn-keycloak-theme/#options-for-customizing-keycloak-ui" class="hash-link" aria-label="Direct link to Options for Customizing Keycloak UI" title="Direct link to Options for Customizing Keycloak UI" translate="no">​</a></h2>
<ol>
<li class=""><strong>FTL Templates</strong>: Keycloak's default theming system using FreeMarker templates. This is the traditional way to customize Keycloak's UI, but it can be cumbersome and lacks modern component-based design. Phase Two provides a <a class="" href="https://phasetwo.io/docs/getting-started/customizing-ui/">simple CSS customization guide</a> for basic styling changes to avoid having to go into FTL templates.</li>
<li class=""><strong>CSS Customization</strong>: You can override the default CSS styles to change the appearance of Keycloak's UI. This is a simple way to apply custom styles, but it doesn't provide the flexibility of a component-based approach. Phase Two provides a <a class="" href="https://phasetwo.io/docs/getting-started/customizing-ui/">simple CSS customization guide</a> for basic styling changes to base themes.</li>
<li class=""><strong>Keycloakify</strong>: A modern theming solution that allows you to build Keycloak themes using React components. It provides a more flexible and maintainable way to customize Keycloak's UI, leveraging the power of React and modern web development practices.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="built-in-keycloak-themes">Built-in Keycloak Themes<a href="https://phasetwo.io/blog/shadcn-keycloak-theme/#built-in-keycloak-themes" class="hash-link" aria-label="Direct link to Built-in Keycloak Themes" title="Direct link to Built-in Keycloak Themes" translate="no">​</a></h2>
<p>The built-in Keycloak themes are built using not just FTL templates, but use <a href="https://www.patternfly.org/" target="_blank" rel="noopener noreferrer" class="">PatternFly</a> as a design library. PatternFly provides a set of components and styles that are used to build the Keycloak UI. However, it can be difficult to customize these components to fit your specific design needs. The semantics and structure is not always clear, and the CSS can be difficult to override. Components use variables without consistency and getting that right can be a challenge, leaving you wondering if you covered all your use cases.</p>
<p>In addition, there is no easy way to "preview" all variations of the pages. Keycloakify provides a built-in Storybook preview for all page variations.</p>
<p>If you follow the official <a href="https://www.keycloak.org/ui-customization/themes" target="_blank" rel="noopener noreferrer" class="">Guides for theme ui-customization</a> you will quickly find that the dev experience is an after-thought. There is only one sentence about development: "During development you can copy the theme to the themes directory,...". Knowing how the theme inherits from the base, how to override styles, add components, what data is available, or preview a page, is not documented or easy.</p>
<p>As Keycloak releases new versions, the themes are not guaranteed to be compatible. This means that you will have to review and possibly update your custom theme with each Keycloak release, which can be a significant effort.</p>
<p>Lastly, adjusting the Account UI or the Emails, is documented to the extent of changing message templates. As something almost every Keycloak user needs, this leaves a major hole in the theming experience.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="where-keycloakify-fits-in">Where Keycloakify Fits In<a href="https://phasetwo.io/blog/shadcn-keycloak-theme/#where-keycloakify-fits-in" class="hash-link" aria-label="Direct link to Where Keycloakify Fits In" title="Direct link to Where Keycloakify Fits In" translate="no">​</a></h2>
<p><a href="https://www.keycloakify.dev/" target="_blank" rel="noopener noreferrer" class="">Keycloakify</a> is a theming solution that allows you a couple methods, CSS or custom components, of configuration to customize the three main areas of Keycloak:</p>
<ol>
<li class=""><strong>Login Pages</strong></li>
<li class=""><strong>Account UI</strong></li>
<li class=""><strong>Emails</strong></li>
</ol>
<p>It is backwards compatible so that as new Keycloak versions are released, you can continue to use your custom theme without having to worry about breaking changes. It also provides a built-in Storybook preview for all page variations, making it easy to see how your customizations will look in different scenarios.</p>
<p>Of the two methods that Keycloakify provides, we see that almost in all cases, the custom components method ends up being used. Dealing with PatternFly's CSS is difficult and error prone. That being said, Keycloakify requires some initial configuration to do custom component development: adding libraries, ejecting pages, etc. For this reason, we are taking a baseline starter and sharing it with the community in the hopes that it will help save people time in getting started with Keycloakify.</p>
<p>Also, we know that eventually there will be development to remove the need for this starter, but until then, we hope this will help you get started with Keycloakify and building custom themes for Keycloak.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="summary">Summary<a href="https://phasetwo.io/blog/shadcn-keycloak-theme/#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>We look forward to seeing how the community uses this starter to build custom themes for Keycloak. If you have any questions or feedback, please feel free to file an issue on the <a href="https://github.com/p2-inc/keycloakify-starter-shadcn" target="_blank" rel="noopener noreferrer" class="">GitHub repository</a> or <a class="" href="https://phasetwo.io/contact/">reach out</a>.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>theme</category>
            <category>shadcn</category>
            <category>tailwind</category>
        </item>
        <item>
            <title><![CDATA[Phase Two Recognized as Official CockroachDB Partner]]></title>
            <link>https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/</link>
            <guid>https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/</guid>
            <pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<div]]></description>
            <content:encoded><![CDATA[<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px"><img src="https://phasetwo.io/blog/crdb_partnership/crdb.webp" alt="CockroachDB logo" style="box-shadow:none"><span>+</span><img src="https://phasetwo.io/img/logo-phasetwo.png" style="box-shadow:none" alt="Phase Two Logo"></div>
<p><a href="https://phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">Phase Two</a> is thrilled to announce that we have been recognized as an <strong>official partner</strong> of <a href="https://www.cockroachlabs.com/partners/locator/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a>. This partnership marks a significant milestone in our commitment to providing robust, scalable, and high-performance database solutions for our the Keycloak community and our customers.</p>
<p>Phase Two originally built our <a href="https://github.com/p2-inc?q=crdb&amp;type=all&amp;language=&amp;sort=#our-docker-images" target="_blank" rel="noopener noreferrer" class="">CockroachDB integration</a> for <a href="https://quay.io/repository/phasetwo/keycloak-crdb" target="_blank" rel="noopener noreferrer" class="">Keycloak</a> over two years ago, and since then we have been working closely with the CockroachDB team to ensure that our integration is optimized for performance and reliability. Our customers have seen significant improvements in database performance, scalability, and a overall cost savings by using CockroachDB with Keycloak.</p>
<p>Our commitment to CockroachDB is built on years of using the <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">Cockroach Cloud</a> to power all of our dedicated <a href="https://phasetwo.io/product/hosting" target="_blank" rel="noopener noreferrer" class="">hosting</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-is-cockroachdb">What is CockroachDB?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#what-is-cockroachdb" class="hash-link" aria-label="Direct link to What is CockroachDB?" title="Direct link to What is CockroachDB?" translate="no">​</a></h2>
<p><a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a> is a cloud-native, distributed SQL database designed for high availability, scalability, and resilience. It provides a single, unified database platform that can handle large volumes of data across multiple geographic locations while ensuring strong consistency and fault tolerance. CockroachDB is built on a distributed architecture that allows it to scale horizontally, making it an ideal choice for applications that require high performance and reliability. Payments systems, IAM, logistics, user accounts, and more — are powered by CockroachDB. Mission-critical applications rely on CockroachDB to deliver consistent performance and availability, scaling to meet demand and transaction heavy workloads.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="why-keycloak-and-cockroachdb">Why Keycloak and CockroachDB?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#why-keycloak-and-cockroachdb" class="hash-link" aria-label="Direct link to Why Keycloak and CockroachDB?" title="Direct link to Why Keycloak and CockroachDB?" translate="no">​</a></h2>
<p>Keycloak is a powerful open-source identity and access management solution that provides authentication and authorization services for applications and services. It is designed to be flexible, extensible, and easy to use, making it a popular choice for organizations looking to implement secure user management.</p>
<p>When combined with CockroachDB, Keycloak benefits from the following:</p>
<ul>
<li class=""><strong>Scalability</strong>: CockroachDB's distributed architecture allows Keycloak to scale horizontally, handling large volumes of user data and authentication requests without compromising performance.</li>
<li class=""><strong>Heavy Transaction Support</strong>: CockroachDB is designed to handle high transaction volumes, making it suitable for Keycloak deployments support millions of users and high-frequency authentication requests.</li>
<li class=""><strong>High Availability</strong>: CockroachDB's fault-tolerant design ensures that Keycloak remains available even in the event of hardware failures or network issues, providing a reliable user experience.</li>
<li class=""><strong>Strong Consistency</strong>: CockroachDB's distributed SQL capabilities ensure that Keycloak can maintain strong consistency across multiple nodes, preventing data inconsistencies and ensuring accurate user management.</li>
<li class=""><strong>Cost Efficiency</strong>: CockroachDB's ability to scale horizontally means that organizations can optimize their infrastructure costs while still meeting the demands of their Keycloak deployments.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="real-world-latency-performance">Real-World Latency Performance<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#real-world-latency-performance" class="hash-link" aria-label="Direct link to Real-World Latency Performance" title="Direct link to Real-World Latency Performance" translate="no">​</a></h3>
<p>One of the key performance indicators we monitor in our production environments is query latency — a critical factor for identity systems like Keycloak, where responsiveness directly impacts user experience.</p>
<p>Over the past month (July 2025), our Keycloak deployments running on CockroachDB have consistently demonstrated impressive latency metrics:</p>
<ul>
<li class=""><strong>P99 Latency</strong> (99th percentile) consistently remained <strong>below 10ms</strong>, even during traffic spikes.</li>
<li class=""><strong>P90 Latency</strong> (90th percentile) hovered well under 5ms across the board.</li>
<li class=""><strong>Spikes</strong>: Occasional spikes up to 70ms (but usually below 35ms) for P99 and 15ms for P90 were observed lasting 5-60 mins with quick recovery afterward.</li>
</ul>
<p>These low-latency metrics are a testament to the <strong>combined performance of CockroachDB’s distributed SQL engine</strong> and our infrastructure tuning, ensuring fast and reliable authentication and authorization at scale.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="phase-twos-commitment-to-cockroachdb">Phase Two's Commitment to CockroachDB<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#phase-twos-commitment-to-cockroachdb" class="hash-link" aria-label="Direct link to Phase Two's Commitment to CockroachDB" title="Direct link to Phase Two's Commitment to CockroachDB" translate="no">​</a></h2>
<p>As the original builders and continued maintainers of the Keycloak CockroachDB integration, Phase Two is committed to supporting the integration and ensuring that it meets the evolving needs of our customers. Our partnership with CockroachDB is a cementing of that commitment, and we are excited to work together to deliver the best possible experience for our users.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-this-means-for-our-customers">What This Means for Our Customers<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#what-this-means-for-our-customers" class="hash-link" aria-label="Direct link to What This Means for Our Customers" title="Direct link to What This Means for Our Customers" translate="no">​</a></h3>
<p>For customers already leveraging CockroachDB in their applications, this partnership ensures even deeper integration and support for running Keycloak on the same trusted, resilient database platform. You can expect streamlined operations, improved compatibility, and direct access to expertise from both the Phase Two and CockroachDB teams.</p>
<p>For those new to CockroachDB or considering it for the first time, our partnership means you can confidently adopt a modern, distributed database solution for your Keycloak deployments. Phase Two will guide you through best practices, migration strategies, and ongoing support to help you realize the benefits of CockroachDB’s scalability, reliability, and performance.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="customer-success-story---grocery-store-chain">Customer Success Story - Grocery Store Chain<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#customer-success-story---grocery-store-chain" class="hash-link" aria-label="Direct link to Customer Success Story - Grocery Store Chain" title="Direct link to Customer Success Story - Grocery Store Chain" translate="no">​</a></h3>
<blockquote>
<p>A customer of Phase Two, a <strong>major US grocery store chain</strong>, had zero experience with CockroachDB before partnering with us. They were running on legacy Oracle systems and were looking for a modern, scalable, cloud solution. Leveraging our CRDB integration, we were able to migrate their Ping implementation to Keycloak with CockroachDB, resulting in a major cost reduction and improved performance. Their system is now running smoothly with millions of users and transactions. Those Friday coupon releases are now a breeze!</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="customer-success-story---global-leader-in-rail-technology">Customer Success Story - Global Leader in Rail Technology<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#customer-success-story---global-leader-in-rail-technology" class="hash-link" aria-label="Direct link to Customer Success Story - Global Leader in Rail Technology" title="Direct link to Customer Success Story - Global Leader in Rail Technology" translate="no">​</a></h3>
<blockquote>
<p>A global leader in rail technology partnered with Phase Two to modernize their identity infrastructure. With operations spanning continents and complex security requirements, they needed a scalable and resilient solution. Using our enhanced Keycloak distribution with native CockroachDB support, they migrated from their legacy system to a platform built for high availability and global scale. Today, their identity layer supports users across critical transportation systems—delivering performance, reliability, and peace of mind, even at full throttle.</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="customer-success-story---global-automobile-and-tire-supplier">Customer Success Story - Global Automobile and Tire Supplier<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#customer-success-story---global-automobile-and-tire-supplier" class="hash-link" aria-label="Direct link to Customer Success Story - Global Automobile and Tire Supplier" title="Direct link to Customer Success Story - Global Automobile and Tire Supplier" translate="no">​</a></h3>
<blockquote>
<p>What started as a open-source user of our CRDB integration, a <strong>global automobile and tire supplier</strong>, has now become a key customer of Phase Two. They started with using our CRDB integration for their Keycloak deployments and have since involved Phase Two in their enterprise Keycloak deployments. Working together, Phase Two has helped to optimize and scale their Keycloak + CockroachDB deployments. We have been able to streamline their deployment processes, integrate or upgrade custom SPI's (or migrate to more effective ones), and integrate use of the Phase Two <a class="" href="https://phasetwo.io/product/organizations/">Organization's extension</a> to better manage their Keycloak users within their system. As a result of the CRDB partnership, we were able to optimize specific Keycloak queries and the CRDB dialect to improve performance within both systems, which would not have been possible without the close collaboration with CockroachDB.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="ready-to-power-your-keycloak-deployment-with-cockroachdb">Ready to Power Your Keycloak Deployment with CockroachDB?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#ready-to-power-your-keycloak-deployment-with-cockroachdb" class="hash-link" aria-label="Direct link to Ready to Power Your Keycloak Deployment with CockroachDB?" title="Direct link to Ready to Power Your Keycloak Deployment with CockroachDB?" translate="no">​</a></h2>
<p><a href="https://phasetwo.io/contact" target="_blank" rel="noopener noreferrer" class="">Contact us</a> to learn how Phase Two can help you implement a scalable, resilient, and high-performing identity infrastructure with CockroachDB and Keycloak.</p>
<p>Or test it out for yourself at <a href="https://dash.phasetwo.io/" target="_blank" rel="noopener noreferrer" class="">dash.phasetwo.io</a> where you can deploy Keycloak with CockroachDB in minutes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Sc0N" id="frequently-asked-questions-faq">Frequently Asked Questions (FAQ)<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#frequently-asked-questions-faq" class="hash-link" aria-label="Direct link to Frequently Asked Questions (FAQ)" title="Direct link to Frequently Asked Questions (FAQ)" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="what-is-the-advantage-of-using-cockroachdb-with-keycloak">What is the advantage of using CockroachDB with Keycloak?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#what-is-the-advantage-of-using-cockroachdb-with-keycloak" class="hash-link" aria-label="Direct link to What is the advantage of using CockroachDB with Keycloak?" title="Direct link to What is the advantage of using CockroachDB with Keycloak?" translate="no">​</a></h3>
<p>Using CockroachDB with Keycloak improves horizontal scalability, high availability, and ensures strong consistency for identity data across distributed systems.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="can-i-migrate-an-existing-keycloak-deployment-to-cockroachdb">Can I migrate an existing Keycloak deployment to CockroachDB?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#can-i-migrate-an-existing-keycloak-deployment-to-cockroachdb" class="hash-link" aria-label="Direct link to Can I migrate an existing Keycloak deployment to CockroachDB?" title="Direct link to Can I migrate an existing Keycloak deployment to CockroachDB?" translate="no">​</a></h3>
<p>Yes. Phase Two provides migration tools and support for transitioning your Keycloak deployment to CockroachDB.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="is-cockroachdb-a-drop-in-replacement-for-postgresql-in-keycloak">Is CockroachDB a drop-in replacement for PostgreSQL in Keycloak?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#is-cockroachdb-a-drop-in-replacement-for-postgresql-in-keycloak" class="hash-link" aria-label="Direct link to Is CockroachDB a drop-in replacement for PostgreSQL in Keycloak?" title="Direct link to Is CockroachDB a drop-in replacement for PostgreSQL in Keycloak?" translate="no">​</a></h3>
<p>Yes. CockroachDB with Phase Two's integration, it can serve as a drop-in replacement in most Keycloak deployments default PostgreSQL implementation.</p>
<h3 class="anchor anchorTargetStickyNavbar_Sc0N" id="does-phase-two-offer-support-for-running-keycloak-on-cockroachdb">Does Phase Two offer support for running Keycloak on CockroachDB?<a href="https://phasetwo.io/blog/phasetwo-cockroachdb-partnership/#does-phase-two-offer-support-for-running-keycloak-on-cockroachdb" class="hash-link" aria-label="Direct link to Does Phase Two offer support for running Keycloak on CockroachDB?" title="Direct link to Does Phase Two offer support for running Keycloak on CockroachDB?" translate="no">​</a></h3>
<p>Yes. As official CockroachDB partners, Phase Two offers enterprise-grade support, consulting, and managed services for Keycloak + CockroachDB.</p>]]></content:encoded>
            <author>support@phasetwo.io (Phase Two)</author>
            <category>phase_two</category>
            <category>keycloak</category>
            <category>cockroachdb</category>
            <category>partnership</category>
            <category>database</category>
            <category>performance</category>
        </item>
    </channel>
</rss>