<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Edouard Maleix</title>
    <description>The latest articles on DEV Community by Edouard Maleix (@getlarge).</description>
    <link>https://dev.to/getlarge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg</url>
      <title>DEV Community: Edouard Maleix</title>
      <link>https://dev.to/getlarge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/getlarge"/>
    <language>en</language>
    <item>
      <title>Securing MCP Servers with OAuth2: Ory Hydra + Claude Code + ChatGPT</title>
      <dc:creator>Edouard Maleix</dc:creator>
      <pubDate>Fri, 30 Jan 2026 15:18:26 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/securing-mcp-servers-with-oauth2-ory-hydra-claude-code-chatgpt-58hm</link>
      <guid>https://dev.to/playfulprogramming/securing-mcp-servers-with-oauth2-ory-hydra-claude-code-chatgpt-58hm</guid>
      <description>&lt;p&gt;A client asked me to secure their MCP server. Simple enough — throw in some API keys, right?&lt;/p&gt;

&lt;p&gt;But the Model Context Protocol spec had other ideas: OAuth2 with Dynamic Client Registration, Resource Indicators (RFC 8707), Protected Resource Metadata (RFC 9728)...&lt;/p&gt;

&lt;p&gt;One week and dozens of authentication bugs later, I have an MCP server that works with Claude and ChatGPT. Here's everything I learned — so you don't have to repeat the dance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use API Keys?
&lt;/h2&gt;

&lt;p&gt;You're building an MCP server — an API that lets AI assistants like Claude and ChatGPT call external tools. Your first instinct: add a simple &lt;code&gt;Authorization: Bearer sk-...&lt;/code&gt; header and call it a day.&lt;/p&gt;

&lt;p&gt;Then you read the &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization spec (2025-11-25 revision)&lt;/a&gt; and realize&lt;sup id="fnref1"&gt;1&lt;/sup&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth 2.1 (&lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-14" rel="noopener noreferrer"&gt;draft-ietf-oauth-v2-1&lt;/a&gt;) is &lt;strong&gt;MUST&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Client ID Metadata Documents (&lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00" rel="noopener noreferrer"&gt;draft-ietf-oauth-client-id-metadata-document&lt;/a&gt;) is &lt;strong&gt;SHOULD&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dynamic Client Registration (&lt;a href="https://datatracker.ietf.org/doc/html/rfc7591" rel="noopener noreferrer"&gt;RFC 7591&lt;/a&gt;) is &lt;strong&gt;MAY&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Resource Indicators (&lt;a href="https://datatracker.ietf.org/doc/html/rfc8707" rel="noopener noreferrer"&gt;RFC 8707&lt;/a&gt;) is &lt;strong&gt;MUST&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Protected Resource Metadata (&lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt;) is &lt;strong&gt;MUST&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Authorization Server Metadata (&lt;a href="https://datatracker.ietf.org/doc/html/rfc8414" rel="noopener noreferrer"&gt;RFC 8414&lt;/a&gt;) is &lt;strong&gt;MUST&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"This is overkill for my side project," you think.&lt;/p&gt;

&lt;p&gt;But here's the reality: &lt;strong&gt;major MCP clients already implement these specs&lt;/strong&gt; — Claude, ChatGPT, LibreChat, and others. Your MCP server either speaks OAuth2 correctly, or it doesn't work with any of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why OAuth2 Matters for MCP Servers
&lt;/h3&gt;

&lt;p&gt;If you're new to OAuth2: think of it as a crowd of services dancing together — one misstep and the whole routine falls apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExdnZ5OGJudG44Y3I4N2M4Mm54NzhrbG84N2gzcGg1NG9jcHBncWU0YyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F3ohjV6EaqkKjhQxlwk%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExdnZ5OGJudG44Y3I4N2M4Mm54NzhrbG84N2gzcGg1NG9jcHBncWU0YyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F3ohjV6EaqkKjhQxlwk%2Fgiphy.gif" alt="Lone squirrel dancing" width="272" height="480"&gt;&lt;/a&gt;&lt;br&gt;You, when you implement OAuth2 for the first time
  &lt;/p&gt;

&lt;p&gt;In our setup, the crowd includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your MCP Server&lt;/strong&gt; (the API you're protecting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ory Network&lt;/strong&gt; (the bouncer checking credentials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; (proving who you are via social login)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP clients like Claude, ChatGPT, or LibreChat&lt;/strong&gt; (requesting access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each dancer has a role, and OAuth2 is the choreography that makes them work together without your MCP server ever seeing passwords.&lt;/p&gt;

&lt;p&gt;Why go through this trouble instead of simple API keys? OAuth2 solves real security problems:&lt;/p&gt;

&lt;h4&gt;
  
  
  User Context
&lt;/h4&gt;

&lt;p&gt;MCP servers need to know &lt;em&gt;who&lt;/em&gt; is making requests. Without proper auth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All users share the same data (no privacy)&lt;/li&gt;
&lt;li&gt;No audit trail (who did what?)&lt;/li&gt;
&lt;li&gt;Can't implement rate limiting per user&lt;/li&gt;
&lt;li&gt;Resource ownership is impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Token Lifecycle
&lt;/h4&gt;

&lt;p&gt;OAuth2 access tokens expire by design. API keys &lt;em&gt;can&lt;/em&gt; have expiration dates too, but in practice most don't — and even when they do, rotation is manual and easy to forget. OAuth2 bakes this in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived access tokens limit the blast radius of a compromise&lt;/li&gt;
&lt;li&gt;Refresh tokens enable seamless rotation without user disruption&lt;/li&gt;
&lt;li&gt;Revocation is immediate (especially with opaque tokens + introspection)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Scope Limitation
&lt;/h4&gt;

&lt;p&gt;OAuth2 tokens carry scopes (permissions):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;openid&lt;/code&gt; = identity information&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;offline_access&lt;/code&gt; = refresh tokens&lt;/li&gt;
&lt;li&gt;Custom scopes = fine-grained access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single API key is all-or-nothing. OAuth2 tokens can be scoped to specific operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Standard Protocol
&lt;/h4&gt;

&lt;p&gt;MCP clients already implement OAuth2. If you use API keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need a custom authentication flow&lt;/li&gt;
&lt;li&gt;MCP clients won't auto-discover your server&lt;/li&gt;
&lt;li&gt;You're maintaining non-standard auth code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security by simplicity&lt;/strong&gt;: Using OAuth2 means following a proven standard instead of rolling your own authentication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Okay, I'm lying — OAuth2 is complicated.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Learning by Doing
&lt;/h2&gt;

&lt;p&gt;To make sense of all this, let's build a minimal MCP server secured with OAuth2 using Ory Hydra and Kratos. By the end, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ OAuth2 authorization and consent (Hydra)&lt;/li&gt;
&lt;li&gt;✅ Social login via GitHub (Kratos)&lt;/li&gt;
&lt;li&gt;⚠️ Works with Claude Code CLI (with caveats - see Bug #2)&lt;/li&gt;
&lt;li&gt;✅ Works with Claude.ai Custom Connectors&lt;/li&gt;
&lt;li&gt;✅ Works with Claude Desktop&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Works with ChatGPT Web App&lt;/strong&gt; (tested - actually works better than Claude!)&lt;/li&gt;
&lt;li&gt;❌ Codex VSCode extension (OAuth not supported for custom servers)&lt;/li&gt;
&lt;li&gt;✅ User-scoped resource isolation (you only see your data)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Stack: What Each Piece Does
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbv5o02d06swb6g11431q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbv5o02d06swb6g11431q.png" alt="Diagram of OAuth2 flow between Client, MCP Server, Ory Hydra, and Ory Kratos" width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this stack?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ory Kratos and Hydra&lt;/strong&gt;: Identity management and OAuth2 server &lt;a href="https://github.com/ory/hydra" rel="noopener noreferrer"&gt;trusted by companies like OpenAI&lt;/a&gt; for scale and security&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastify MCP&lt;/strong&gt;: Fastify plugin, forked from &lt;a href="https://github.com/platformatic/mcp" rel="noopener noreferrer"&gt;@platformatic/mcp&lt;/a&gt; with better OAuth2 support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1: Setting Up Ory Network
&lt;/h2&gt;

&lt;p&gt;I use &lt;strong&gt;Ory Network&lt;/strong&gt; for this guide. It hosts your Hydra and Kratos instances so you don't have to manage infrastructure.&lt;br&gt;
It's the same team behind Ory Hydra, Kratos, Keto, and Oathkeeper.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Yes, they have a free Developer tier.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 1: Create Your Ory Network Project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Register account&lt;/strong&gt;: Visit &lt;a href="https://console.ory.sh" rel="noopener noreferrer"&gt;https://console.ory.sh&lt;/a&gt; and sign up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create workspace&lt;/strong&gt;: Click "New Workspace" (organizational container for projects)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create project&lt;/strong&gt;: Within your workspace, click "New Project"

&lt;ul&gt;
&lt;li&gt;Name: "MCP Server Development"&lt;/li&gt;
&lt;li&gt;Region: Choose closest to you&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate API key&lt;/strong&gt;: Go to &lt;strong&gt;Developers&lt;/strong&gt; tab → &lt;strong&gt;API Keys&lt;/strong&gt; → "Create API Key"

&lt;ul&gt;
&lt;li&gt;Save the key securely (you'll need it for introspection auth)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I can feel your sarcasm: "We use an API key to avoid API keys?" Yes, but this key is for Ory Network management only, not for your MCP server clients.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Note your project details&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace ID: (in workspace settings)&lt;/li&gt;
&lt;li&gt;Project ID: (in project settings)&lt;/li&gt;
&lt;li&gt;Project slug: &lt;code&gt;{your-slug}.projects.oryapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Detailed setup guide&lt;/strong&gt;: See &lt;a href="https://dev.to/getlarge/deployment-to-ory-network-38ml"&gt;Deployment to Ory Network&lt;/a&gt; for complete walkthrough including CLI setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer running everything locally?&lt;/strong&gt; Skip Ory Network and use Docker Compose. Your configuration is fully portable to self-hosted Hydra/Kratos later — I tested both, and the same JSON config works on either. This is the beauty of Ory.&lt;br&gt;
This &lt;a href="https://github.com/getlarge/ticketing/blob/main/docker-compose.yaml" rel="noopener noreferrer"&gt;compose file&lt;/a&gt; is from an older project but demonstrates the full Ory stack (Hydra, Kratos, PostgreSQL, Self-Service UI).&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Understanding OAuth2 Client Registration for MCP
&lt;/h3&gt;

&lt;p&gt;Before configuring, let's understand &lt;strong&gt;why MCP needs Dynamic Client Registration (DCR)&lt;/strong&gt;. (The spec looks over-engineered — until you realize it solves real problems.)&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;"I just want to call an API, why do I need to read so many RFCs?"&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  The Problem: Unknown Clients
&lt;/h4&gt;

&lt;p&gt;Traditional OAuth2 assumes you know your clients upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer manually registers app in OAuth console&lt;/li&gt;
&lt;li&gt;Gets hardcoded &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ships these credentials in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But with MCP&lt;/strong&gt;: When someone discovers your server URL, their Claude client has &lt;strong&gt;never heard of your server&lt;/strong&gt;. How do they get credentials? You can't expect every user to register manually in your OAuth console — that defeats the whole "seamless AI integration" promise.&lt;/p&gt;
&lt;h4&gt;
  
  
  Solution 1: Dynamic Client Registration (DCR - RFC 7591)
&lt;/h4&gt;

&lt;p&gt;DCR lets clients register themselves &lt;strong&gt;at runtime&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client discovers MCP server&lt;/li&gt;
&lt;li&gt;Client reads &lt;code&gt;registration_endpoint&lt;/code&gt; from OIDC discovery&lt;/li&gt;
&lt;li&gt;Client POSTs metadata to register: &lt;code&gt;{redirect_uris, grant_types, scopes}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Server returns new &lt;code&gt;client_id&lt;/code&gt; (and maybe &lt;code&gt;client_secret&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Client proceeds with normal OAuth flow using the registered scopes&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  Solution 2: Client ID Metadata Documents (CIMD)
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents" rel="noopener noreferrer"&gt;November 2025 MCP spec&lt;/a&gt; introduced CIMD as the &lt;strong&gt;SHOULD&lt;/strong&gt; (recommended) approach, with DCR as &lt;strong&gt;MAY&lt;/strong&gt; (optional fallback).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Clients publish metadata at an HTTPS URL they control. &lt;strong&gt;That URL becomes the client_id&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Example metadata at &lt;code&gt;https://app.example.com/oauth/client-metadata.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.example.com/oauth/client-metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My MCP Client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grant_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redirect_uris"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000/callback"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;During OAuth flow&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client sends &lt;code&gt;client_id=https://app.example.com/oauth/client-metadata.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Authorization server fetches that URL&lt;/li&gt;
&lt;li&gt;Server validates: &lt;code&gt;client_id&lt;/code&gt; matches URL, &lt;code&gt;redirect_uri&lt;/code&gt; is in allowed list&lt;/li&gt;
&lt;li&gt;Server caches metadata (respecting HTTP cache headers)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Comparison&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;DCR&lt;/th&gt;
&lt;th&gt;CIMD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP Spec Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MAY (optional)&lt;/td&gt;
&lt;td&gt;SHOULD (recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-minted UUID&lt;/td&gt;
&lt;td&gt;Client's HTTPS URL (e.g., &lt;code&gt;/client.json&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Registration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;POST to server&lt;/td&gt;
&lt;td&gt;Self-published JSON document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stores records in database&lt;/td&gt;
&lt;td&gt;Fetches on-demand, caches via HTTP headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local agents without reliable URLs&lt;/td&gt;
&lt;td&gt;Web/hosted clients with stable domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Localhost support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Works fine&lt;/td&gt;
&lt;td&gt;⚠️ Risk: anyone can claim localhost URIs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;For this guide&lt;/strong&gt;: I use &lt;strong&gt;DCR&lt;/strong&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local agents (Claude CLI, CLI tools) don't have stable HTTPS URLs to host metadata&lt;/li&gt;
&lt;li&gt;Ory Hydra supports DCR out of the box&lt;/li&gt;
&lt;li&gt;DCR works for both localhost and production scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CIMD note&lt;/strong&gt;: Authorization servers advertise CIMD support via &lt;code&gt;client_id_metadata_document_supported: true&lt;/code&gt; in their metadata. Ory Hydra &lt;a href="https://github.com/ory/hydra/issues/4061" rel="noopener noreferrer"&gt;doesn't support this yet&lt;/a&gt;, but if you're building a web-based MCP client with a stable domain, CIMD is the preferred approach per the MCP spec.&lt;/p&gt;


&lt;h3&gt;
  
  
  Ory Configuration Reference
&lt;/h3&gt;

&lt;p&gt;Here are the key configurations you need. In Ory Network, the project config is a single JSON document that merges settings from all Ory services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;services.oauth2&lt;/code&gt;&lt;/strong&gt; → Ory Hydra (OAuth2/OIDC server)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;services.identity&lt;/code&gt;&lt;/strong&gt; → Ory Kratos (Identity management, social login)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;services.permission&lt;/code&gt;&lt;/strong&gt; → Ory Keto (Authorization, not used in this guide)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are &lt;strong&gt;two ways&lt;/strong&gt; to apply these configs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import JSON via CLI&lt;/strong&gt; (fastest - paste the whole merged config)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure via Console UI&lt;/strong&gt; (guided, click-through for each service)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  OAuth2 Configuration (Hydra)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"pkce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enforced"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enforced_for_public_clients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"oidc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"dynamic_client_registration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"default_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"offline_access"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"strategies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"opaque"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wildcard"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ttl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1h0m0s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"720h0m0s"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"webfinger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"oidc_discovery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"auth_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://{slug}.projects.oryapis.com/oauth2/auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"client_registration_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://{slug}.projects.oryapis.com/oauth2/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"jwks_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://{slug}.projects.oryapis.com/.well-known/jwks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"token_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://{slug}.projects.oryapis.com/oauth2/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"userinfo_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://{slug}.projects.oryapis.com/userinfo"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Key settings explained&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;access_token: "opaque"&lt;/code&gt; - Use opaque tokens (revocable, secure)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dynamic_client_registration.enabled: true&lt;/code&gt; - Enables DCR for MCP clients&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pkce.enforced_for_public_clients: true&lt;/code&gt; - Enforces PKCE for CLI clients like Claude Code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webfinger.oidc_discovery.client_registration_url&lt;/code&gt; - Can override for DCR proxy (see Bug #1 workaround)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why opaque tokens?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Immediate revocation (logout, compromised tokens)&lt;/li&gt;
&lt;li&gt;No JWT parsing vulnerabilities&lt;/li&gt;
&lt;li&gt;Token contents stay server-side&lt;/li&gt;
&lt;li&gt;Ory's introspection is fast and cached&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Identity Configuration (Kratos - for GitHub login)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"selfservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"methods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"oidc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_GITHUB_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_GITHUB_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user:email"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;See "Part 2: GitHub Social Login" below for how to get GitHub OAuth credentials.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 2: Apply Configuration
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Option A: Import Config via Ory CLI (Fastest)
&lt;/h4&gt;

&lt;p&gt;Save the merged config (OAuth2 + Identity sections) to a file and &lt;a href="https://www.ory.com/docs/cli/ory-update-project" rel="noopener noreferrer"&gt;update your Ory project&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Save your config to ory-config.json&lt;/span&gt;
&lt;span class="c"&gt;# (Merge the OAuth2 and Identity configs from above)&lt;/span&gt;

&lt;span class="c"&gt;# Apply to your Ory project&lt;/span&gt;
ory update project &amp;lt;project-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workspace&lt;/span&gt; &amp;lt;workspace-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; ory-config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hint&lt;/strong&gt;: You can &lt;a href="https://www.ory.com/docs/cli/ory-get-project" rel="noopener noreferrer"&gt;export&lt;/a&gt; your current config with:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ory get project &amp;lt;project-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workspace&lt;/span&gt; &amp;lt;workspace-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; current-ory-config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Done!&lt;/strong&gt; Your Ory project is now MCP-ready.&lt;/p&gt;

&lt;p&gt;
  &lt;strong&gt;Option B: Configure via Console UI (Guided)&lt;/strong&gt;
  &lt;p&gt;Prefer clicking through the UI? Each setting can be configured in the Ory Console:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1 Enable Dynamic Client Registration&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;OAuth2 &amp;amp; OpenID Connect&lt;/strong&gt; → &lt;strong&gt;Configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Dynamic Client Registration&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Default Scopes&lt;/strong&gt;: &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;offline_access&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.2 Set Token Strategy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;OAuth2 &amp;amp; OpenID Connect&lt;/strong&gt; → &lt;strong&gt;Advanced&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Token Strategy&lt;/strong&gt;: Choose &lt;strong&gt;Opaque&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.3 Enable PKCE&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;OAuth2 &amp;amp; OpenID Connect&lt;/strong&gt; → &lt;strong&gt;Security&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;PKCE for Public Clients&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.4 Set Token TTLs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;OAuth2 &amp;amp; OpenID Connect&lt;/strong&gt; → &lt;strong&gt;Token TTL&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Access Token: &lt;code&gt;1h&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Refresh Token: &lt;code&gt;720h&lt;/code&gt; (30 days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.5 Enable OIDC for Identity (Social Login)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Identity&lt;/strong&gt; → &lt;strong&gt;Social Sign-In&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Provider&lt;/strong&gt; → &lt;strong&gt;GitHub&lt;/strong&gt; (or your preferred provider)&lt;/li&gt;
&lt;li&gt;Enter Client ID and Client Secret (see GitHub setup)&lt;/li&gt;
&lt;li&gt;Set Scopes: &lt;code&gt;user:email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Create a Static OAuth2 Client
&lt;/h3&gt;

&lt;p&gt;Create a static OAuth2 client for testing and for use with Claude Desktop or Claude.ai. Even if you plan to use DCR in production, having a static client helps you debug the OAuth flow without worrying about DCR-specific issues (like Bug #1).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Via Ory Console&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;OAuth2 &amp;amp; OpenID Connect&lt;/strong&gt; → &lt;strong&gt;OAuth2 Clients&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Client&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fill in:

&lt;ul&gt;
&lt;li&gt;Name: "Manual Test Client"&lt;/li&gt;
&lt;li&gt;Grant Types: Authorization Code, Refresh Token&lt;/li&gt;
&lt;li&gt;Redirect URIs: &lt;code&gt;http://localhost:8000/oauth/callback&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scopes: &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;offline_access&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save and note the &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Via Ory CLI&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ory create oauth2-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;your-project-id&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Manual Test Client"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--grant-type&lt;/span&gt; authorization_code,refresh_token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--response-type&lt;/span&gt; code &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; openid,offline_access &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--redirect-uri&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000/oauth/callback"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;💡 &lt;strong&gt;Keep these credentials&lt;/strong&gt; - you'll use them for testing in Part 4.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 2: GitHub Social Login (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;
  Expand GitHub Social Login setup
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubeh5wxakijz6hhp4yji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubeh5wxakijz6hhp4yji.png" alt="GitHub Login Screenshot" width="800" height="918"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why GitHub?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No password management to worry about&lt;/li&gt;
&lt;li&gt;Familiar login flow for developers&lt;/li&gt;
&lt;li&gt;One less thing to debug when testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 1: Create GitHub OAuth App
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/developers" rel="noopener noreferrer"&gt;https://github.com/settings/developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click "New OAuth App"&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fill in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application name&lt;/strong&gt;: MCP Server (Dev)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage URL&lt;/strong&gt;: &lt;code&gt;https://{your-slug}.projects.oryapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization callback URL&lt;/strong&gt;: &lt;code&gt;https://{your-slug}.projects.oryapis.com/self-service/methods/oidc/callback/github&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Save &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 2: Add to Kratos Config
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Via Ory Console&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Identity&lt;/strong&gt; → &lt;strong&gt;Social Sign-In&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Provider&lt;/strong&gt; → &lt;strong&gt;GitHub&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter:

&lt;ul&gt;
&lt;li&gt;Client ID: (from GitHub)&lt;/li&gt;
&lt;li&gt;Client Secret: (from GitHub)&lt;/li&gt;
&lt;li&gt;Scopes: &lt;code&gt;user:email&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Or update via JSON import&lt;/strong&gt; (use the Identity config JSON above with your GitHub credentials).&lt;/p&gt;



&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 3: Your MCP Server with OAuth2
&lt;/h2&gt;

&lt;p&gt;The MCP server is built using Fastify and a fork of &lt;a href="https://github.com/platformatic/mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@platformatic/mcp&lt;/code&gt;&lt;/a&gt; that adds better OAuth2 support. This &lt;a href="https://github.com/getlarge/fastify-mcp/tree/dev" rel="noopener noreferrer"&gt;fork&lt;/a&gt; fixes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OIDC Discovery &amp;amp; OAuth Compliance&lt;/strong&gt; (&lt;a href="https://github.com/platformatic/mcp/pull/97" rel="noopener noreferrer"&gt;PR #97&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original: Hardcoded &lt;code&gt;/oauth/*&lt;/code&gt; paths didn't work with Ory Hydra's non-standard endpoints&lt;/li&gt;
&lt;li&gt;Fixed: Auto-discovers endpoints via &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;, adds &lt;code&gt;redirect_uri&lt;/code&gt; support, allows excluding paths from auth (e.g., &lt;code&gt;/health&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resource Subscriptions&lt;/strong&gt; (&lt;a href="https://github.com/platformatic/mcp/pull/98" rel="noopener noreferrer"&gt;PR #98&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original: No support for MCP resource subscriptions&lt;/li&gt;
&lt;li&gt;Fixed: Adds subscription/unsubscription handlers and query parameter URI matching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DCR Proxy &amp;amp; Token Introspection Auth&lt;/strong&gt; (&lt;a href="https://github.com/platformatic/mcp/pull/100" rel="noopener noreferrer"&gt;PR #100&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original: &lt;code&gt;/oauth/register&lt;/code&gt; endpoint required auth (chicken-egg problem), no way to authenticate introspection requests&lt;/li&gt;
&lt;li&gt;Fixed: DCR endpoint skips auth, adds &lt;code&gt;introspectionAuth&lt;/code&gt; config for Ory admin API, adds DCR hooks for proxy pattern (needed for Bug #1 workaround)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full disclosure&lt;/strong&gt;: This is my fork, tested through building the &lt;a href="https://github.com/getlarge/claude-api-care-plugins/tree/main/plugins/baume/mcp-server" rel="noopener noreferrer"&gt;Baume MCP server&lt;/a&gt;. The upstream PRs await review, but you need these fixes now.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 1: Install Dependencies
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @getlarge/fastify-mcp fastify @sinclair/typebox @fastify/type-provider-typebox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Create Your Server
&lt;/h3&gt;
&lt;h4&gt;
  
  
  ⚠️ Common Pitfall: Schema Definition
&lt;/h4&gt;

&lt;p&gt;Before you write any tools, know this: &lt;code&gt;@getlarge/fastify-mcp&lt;/code&gt; (and &lt;code&gt;@platformatic/mcp&lt;/code&gt;) require tool input schemas to be &lt;strong&gt;objects at the top level&lt;/strong&gt;. If you need mutually exclusive inputs (e.g., &lt;code&gt;path&lt;/code&gt; OR &lt;code&gt;url&lt;/code&gt;), wrap the union inside an object property:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Won't work - Union at top level&lt;/span&gt;
&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...}),&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...})])&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Works - Union inside object property&lt;/span&gt;
&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This also improves clarity for AI clients (ChatGPT, Claude) that sometimes struggle with flat union schemas. Thank me later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File&lt;/strong&gt;: &lt;code&gt;src/server.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mcpPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@getlarge/fastify-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinclair/typebox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Register MCP plugin with OAuth2 config&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcpPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorizationServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{your-slug}.projects.oryapis.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resourceUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;excludedPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;tokenValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Using introspection for opaque tokens&lt;/span&gt;
      &lt;span class="na"&gt;introspectionEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{your-slug}.projects.oryapis.com/admin/oauth2/introspect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// For Ory Network, authenticate introspection with API key:&lt;/span&gt;
      &lt;span class="na"&gt;introspectionAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bearer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ORY_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;validateAudience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// See "Bug #4: Empty JWT Audience"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mcpAddTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Say hello with user context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hello &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Your user ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🚀 MCP Server running on http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Start Your Server
&lt;/h3&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;






&lt;h2&gt;
  
  
  The Five Bugs I Hit (And Their Fixes)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;War stories section&lt;/strong&gt; — I'm listing these in order of severity, so if you're blocked, start from the top.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bug #1&lt;/strong&gt; blocks &lt;em&gt;all&lt;/em&gt; Claude clients (DCR validation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug #2&lt;/strong&gt; blocks Claude Code CLI specifically (scope parameter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs #3-5&lt;/strong&gt; are configuration issues you'll hit along the way&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Bug #1: All Claude Clients Reject DCR Responses with Empty URI Fields
&lt;/h3&gt;

&lt;p&gt;This was the first wall I hit. Dynamic Client Registration succeeds on Hydra's side — the client gets created, logs look fine. Then every Claude client (Code CLI, Claude.ai, Claude Desktop) crashes with a validation error: &lt;code&gt;client_uri&lt;/code&gt;, &lt;code&gt;logo_uri&lt;/code&gt;, &lt;code&gt;tos_uri&lt;/code&gt;, or &lt;code&gt;contacts&lt;/code&gt; must be parseable/valid.&lt;/p&gt;

&lt;p&gt;I dug into Hydra's DCR response and found the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Empty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Claude's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Zod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contacts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;array&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;validation&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logo_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tos_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Claude's Zod schema expects these fields to be either valid URLs (for &lt;code&gt;*_uri&lt;/code&gt; fields), arrays (for &lt;code&gt;contacts&lt;/code&gt;), or omitted entirely — not empty strings or null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status&lt;/strong&gt;: &lt;a href="https://github.com/anthropics/claude-code/issues/13685" rel="noopener noreferrer"&gt;Open issue #13685&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This affects &lt;strong&gt;all Claude clients&lt;/strong&gt;, not just Claude Code CLI. That makes it different from Bug #2 (scope parameter), which only hits the CLI.&lt;/p&gt;

&lt;p&gt;The frustrating part: you can't configure Ory Hydra to omit these fields. They're part of the DCR spec, and Hydra includes them even when empty. So I built a workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution: DCR Proxy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lightweight proxy between Claude and Hydra that strips the problematic fields before Claude sees them. &lt;code&gt;@getlarge/fastify-mcp&lt;/code&gt; supports this as a built-in hook, so the proxy runs in the same process as your MCP server — no separate deployment needed.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DCR Proxy hook in @getlarge/fastify-mcp&lt;/span&gt;
&lt;span class="c1"&gt;// Registered as client_registration_url in your Ory OIDC discovery config&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/oauth2/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Forward DCR request to Hydra&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hydraResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{slug}.projects.oryapis.com/oauth2/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hydraResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Remove fields that are empty strings or null&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleanedClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Keep non-empty strings, non-null values&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleanedClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Configure Ory to use the proxy&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;In your Ory project config, set the DCR endpoint to your MCP server's proxy endpoint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webfinger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"oidc_discovery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"client_registration_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-mcp-server.com/oauth2/register"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Ory Network allows customizing the &lt;code&gt;client_registration_url&lt;/code&gt; in the OIDC discovery document. Set it to &lt;code&gt;undefined&lt;/code&gt; to hide DCR entirely (if you only use static clients).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt;: Claude never sees the empty URI fields, so Zod validation passes 🫢.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation reference&lt;/strong&gt;: See the &lt;a href="https://github.com/getlarge/fastify-mcp/blob/main/src/routes/auth-routes.ts#L308" rel="noopener noreferrer"&gt;DCR proxy hook in fastify-mcp&lt;/a&gt; or its &lt;a href="https://github.com/getlarge/claude-api-care-plugins/blob/main/plugins/baume/mcp-server/src/server.ts#L148-L164" rel="noopener noreferrer"&gt;usage in the Baume MCP server&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You could also consider using placeholder URLs (e.g., &lt;code&gt;https://example.com&lt;/code&gt;) instead of omitting fields, but omitting is cleaner and avoids confusion.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Bug #2: Claude Code CLI Missing Scope Parameter
&lt;/h3&gt;

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

&lt;p&gt;As of January 2026, Claude Code (CLI) has a &lt;a href="https://github.com/anthropics/claude-code/issues/4540" rel="noopener noreferrer"&gt;known bug&lt;/a&gt; (since July 2025, tagged &lt;code&gt;oncall&lt;/code&gt;) where it doesn't send the &lt;code&gt;scope&lt;/code&gt; parameter during OAuth2 authorization, causing the flow to fail entirely. &lt;strong&gt;Use Claude.ai or Claude Desktop instead&lt;/strong&gt; until Anthropic fixes this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side mitigation attempts&lt;/strong&gt; (all failed):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Injecting scopes in DCR response → Claude Code ignores it&lt;/li&gt;
&lt;li&gt;Using WWW-Authenticate scope parameter → Requires initial token (chicken-egg problem)&lt;/li&gt;
&lt;li&gt;Ory &lt;a href="https://github.com/ory/hydra/issues/1618" rel="noopener noreferrer"&gt;default scopes&lt;/a&gt; → Only works with &lt;strong&gt;client_credentials&lt;/strong&gt; grant, not &lt;strong&gt;authorization_code&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How ironic, Anthropic wrote the MCP Authorization spec that requires proper scope handling, but Claude Code CLI doesn't implement it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Bug #3: Token Validation Configuration Mismatch
&lt;/h3&gt;

&lt;p&gt;I kept getting this error:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The token is malformed. (error code: FAST_JWT_MALFORMED)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It took me some time to realize my MCP server was trying to parse an opaque token as a JWT (using JWKS token validation). Opaque is Hydra's default strategy — and I'd forgotten that when configuring the server.&lt;/p&gt;
&lt;h4&gt;
  
  
  Understanding Token Validation Options
&lt;/h4&gt;

&lt;p&gt;Hydra supports two token strategies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Token Format&lt;/th&gt;
&lt;th&gt;Validation Method&lt;/th&gt;
&lt;th&gt;Trade-offs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;opaque&lt;/strong&gt; (recommended)&lt;/td&gt;
&lt;td&gt;Random string (&lt;code&gt;ory_at_...&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Introspection endpoint&lt;/td&gt;
&lt;td&gt;✅ Immediate revocation&lt;br&gt;✅ Better security&lt;br&gt;❌ Network call per request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;jwt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON Web Token (&lt;code&gt;eyJhbGci...&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;JWKS (signature verification)&lt;/td&gt;
&lt;td&gt;✅ Fast (JWKS response is cached)&lt;br&gt;❌ Can't revoke before expiry&lt;br&gt;❌ Exposed claims&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;For this guide and production&lt;/strong&gt;, I recommend &lt;strong&gt;opaque tokens&lt;/strong&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can revoke tokens immediately (logout, compromised tokens)&lt;/li&gt;
&lt;li&gt;Ory's introspection is fast&lt;/li&gt;
&lt;li&gt;No JWT parsing vulnerabilities&lt;/li&gt;
&lt;li&gt;Token contents stay server-side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MCP server must use the &lt;strong&gt;matching validation method&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If using opaque tokens (recommended)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tokenValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;introspectionEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{project}.projects.oryapis.com/admin/oauth2/introspect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// For Ory Network, authenticate with API key:&lt;/span&gt;
    &lt;span class="na"&gt;introspectionAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bearer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ORY_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Local Hydra: no auth needed if admin API is on localhost&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;If using JWT tokens&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tokenValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;jwksUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{project}.projects.oryapis.com/.well-known/jwks.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Validates JWT signature locally - no network call&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Setting Hydra's token strategy&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For opaque tokens (recommended)&lt;/span&gt;
ory patch oauth2-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; &amp;lt;project-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replace&lt;/span&gt; &lt;span class="s2"&gt;"/strategies/access_token=opaque"&lt;/span&gt;

&lt;span class="c"&gt;# For JWT tokens (if you prefer local validation)&lt;/span&gt;
ory patch oauth2-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; &amp;lt;project-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replace&lt;/span&gt; &lt;span class="s2"&gt;"/strategies/access_token=jwt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bug #4: Empty JWT Audience (RFC 8707 Missing)
&lt;/h3&gt;

&lt;p&gt;Token validation kept failing with this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audience validation failed: expected [http://localhost:8000], got []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The MCP spec is clear — clients &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" rel="noopener noreferrer"&gt;MUST implement Resource Indicators&lt;/a&gt; (RFC 8707). That means sending a &lt;code&gt;resource&lt;/code&gt; parameter during authorization:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /oauth2/auth?...&amp;amp;resource=http://localhost:8000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This binds the token to a specific MCP server via the JWT &lt;code&gt;aud&lt;/code&gt; claim. Except Claude.ai doesn't send it (as of Jan 2026). So the audience comes back empty, and validation fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround&lt;/strong&gt;: Disable audience validation:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tokenValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;validateAudience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Until Claude.ai implements RFC 8707&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;⚠️ Security warning for multi-tenant deployments&lt;/strong&gt;: Tokens issued by your Ory instance can technically be used against &lt;em&gt;any&lt;/em&gt; MCP server using the same issuer. For single-tenant deployments, this is acceptable. For multi-tenant, you &lt;strong&gt;must&lt;/strong&gt; add validation (e.g., check &lt;code&gt;client_id&lt;/code&gt; against known registrations, or validate custom claims).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status&lt;/strong&gt;: Pending Claude.ai update to support RFC 8707.&lt;/p&gt;


&lt;h3&gt;
  
  
  Bug #5: Ory Elements Consent Error (False Positive)
&lt;/h3&gt;

&lt;p&gt;This one wasted my time because the real blocker was Bug #3 (token validation). I didn't know that yet. After clicking "Accept" on the consent screen, Ory Elements throws:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Unhandled&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="nx"&gt;Rejection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Ory&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;Elements&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OAuth2&lt;/span&gt; &lt;span class="nx"&gt;consent&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Except... the flow actually succeeds. Claude receives a valid authorization code, tokens work fine, everything is functional. The UI just shows an error because Ory Elements doesn't handle the redirect properly.&lt;/p&gt;

&lt;p&gt;Ory fixed the &lt;a href="https://github.com/ory/elements/issues/587" rel="noopener noreferrer"&gt;issue #587&lt;/a&gt;, so it should not get in your way in future releases.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 4: Testing the Full OAuth2 Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovek8ocluk1jhxsxuj2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovek8ocluk1jhxsxuj2z.png" alt="OAuth2 Consent UI" width="800" height="1528"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Programmatic Testing with Hydra's Admin API
&lt;/h3&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; test manually: generate a PKCE challenge, open the auth URL in a browser, click through GitHub login, copy the code from the callback, exchange it with curl... but that gets old fast.&lt;/p&gt;

&lt;p&gt;A better approach: use Hydra's Admin API to accept login and consent programmatically — no browser, no identities, no clicking. This is what I use in my &lt;a href="https://github.com/getlarge/claude-api-care-plugins/blob/main/plugins/baume/mcp-server/src/auth/auth.oauth2-e2e-test.ts" rel="noopener noreferrer"&gt;e2e tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start the auth flow&lt;/strong&gt; — &lt;code&gt;GET /oauth2/auth?client_id=...&amp;amp;scope=openid+offline_access&amp;amp;...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept login via Admin API&lt;/strong&gt; — &lt;code&gt;PUT /admin/oauth2/auth/requests/login/accept&lt;/code&gt; with a synthetic &lt;code&gt;subject&lt;/code&gt; (no real user needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept consent via Admin API&lt;/strong&gt; — &lt;code&gt;PUT /admin/oauth2/auth/requests/consent/accept&lt;/code&gt; granting the requested scopes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange the code&lt;/strong&gt; — &lt;code&gt;POST /oauth2/token&lt;/code&gt; as usual&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 2 and 3 are the trick — Hydra's Admin API lets you skip the browser entirely. You get a real authorization code and real tokens, without any identity provider involved.&lt;/p&gt;

&lt;p&gt;Here's the full flow against Ory Network — client creation, PKCE auth code exchange, token introspection, and authenticated MCP calls:&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Want the test script?&lt;/strong&gt; Hit me up — I have a standalone Node.js script that runs this entire flow against Ory Network's Admin API, no browser needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you see your user ID in the response, OAuth2 is working.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About Token Refresh?
&lt;/h3&gt;

&lt;p&gt;In our Ory Hydra setup, access tokens expire after 1 hour. The &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-handling" rel="noopener noreferrer"&gt;MCP spec&lt;/a&gt; says the &lt;strong&gt;client&lt;/strong&gt; handles refresh — your server returns 401 on expiry, and the client uses its refresh token to obtain a new access token from Hydra. A &lt;a href="https://modelcontextprotocol.io/specification/draft/basic/authorization#token-refresh" rel="noopener noreferrer"&gt;draft section&lt;/a&gt; formalizes this, but it's not in the released 2025-11-25 spec yet.&lt;/p&gt;

&lt;p&gt;In practice, &lt;strong&gt;don't count on it working smoothly&lt;/strong&gt;. The Python SDK has a &lt;a href="https://github.com/modelcontextprotocol/python-sdk/issues/1250" rel="noopener noreferrer"&gt;P0 bug&lt;/a&gt; where &lt;code&gt;get_access_token()&lt;/code&gt; returns stale tokens after refresh, and ChatGPT enters a &lt;a href="https://community.openai.com/t/stateless-streamable-http-mcp-server-token-refresh-works-after-short-idle-but-reconnect-loop-after-long-idle/1367543" rel="noopener noreferrer"&gt;reconnect loop&lt;/a&gt; instead of refreshing after long idle periods. &lt;code&gt;@getlarge/fastify-mcp&lt;/code&gt; handles the server side (proper 401s with &lt;code&gt;WWW-Authenticate&lt;/code&gt; headers), but whether each client refreshes correctly is out of your hands.&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Checklist
&lt;/h3&gt;

&lt;p&gt;Before going live:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ory Network&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Configure CORS properly (don't use &lt;code&gt;*&lt;/code&gt; in production)&lt;/li&gt;
&lt;li&gt;[ ] Set token TTLs appropriate for your use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable HTTPS&lt;/li&gt;
&lt;li&gt;[ ] Re-enable audience validation once Claude.ai implements RFC 8707&lt;/li&gt;
&lt;li&gt;[ ] Rate limit the DCR proxy endpoint (anyone can register unlimited clients)&lt;/li&gt;
&lt;li&gt;[ ] Add rate limiting per user&lt;/li&gt;
&lt;li&gt;[ ] Set up request logging and error monitoring&lt;/li&gt;
&lt;li&gt;[ ] Add health checks and uptime monitoring&lt;/li&gt;
&lt;li&gt;[ ] Rotate your Ory API key periodically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What About ChatGPT?
&lt;/h2&gt;

&lt;p&gt;I wasn't planning to test ChatGPT, but after hitting so many bugs with Claude's OAuth2 implementation, I got curious. Turns out ChatGPT Web App handles OAuth2 &lt;strong&gt;better&lt;/strong&gt; than Claude Code.&lt;/p&gt;

&lt;h3&gt;
  
  
  ChatGPT Web App (Tested 2026-01-29)
&lt;/h3&gt;

&lt;p&gt;Full OAuth2.1 with DCR just works. DCR flow is clean — no phantom scopes, proper scope handling, no false errors on redirect. It even surfaces rich tool metadata (&lt;code&gt;PUBLIC WRITE&lt;/code&gt;, &lt;code&gt;OPEN WORLD&lt;/code&gt;, &lt;code&gt;DESTRUCTIVE&lt;/code&gt; annotations) with per-tool auth support.&lt;/p&gt;

&lt;p&gt;To try it yourself: go to ChatGPT web, enable Developer Mode in settings, add your MCP server URL, and the OAuth flow triggers automatically. Complete login + consent via Ory Hydra, and ChatGPT manages token storage and refresh from there — though after long idle periods it can &lt;a href="https://community.openai.com/t/stateless-streamable-http-mcp-server-token-refresh-works-after-short-idle-but-reconnect-loop-after-long-idle/1367543" rel="noopener noreferrer"&gt;break into a reconnect loop&lt;/a&gt; instead of refreshing.&lt;/p&gt;

&lt;p&gt;Your server doesn't need any ChatGPT-specific config. It just needs &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt; and proper &lt;code&gt;WWW-Authenticate&lt;/code&gt; errors on unauthorized requests — same as Claude, and &lt;code&gt;@getlarge/fastify-mcp&lt;/code&gt; handles both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codex VSCode Extension (Tested 2026-01-29)
&lt;/h3&gt;

&lt;p&gt;No luck here. Codex has a two-tier system: recommended servers (Linear, Notion, Figma) get full OAuth with an "Install and authenticate" button, but custom servers only get a toggle — no OAuth flow.&lt;/p&gt;

&lt;p&gt;You can work around it with bearer tokens in &lt;code&gt;~/.codex/config.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.your-server]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://your-server.com/mcp"&lt;/span&gt;
&lt;span class="py"&gt;bearer_token_env_var&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_SERVER_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then manually obtain a token via Hydra and set &lt;code&gt;export YOUR_SERVER_TOKEN="ory_at_..."&lt;/code&gt;. But the token expires after 1 hour with no auto-refresh, rotation is manual, and there's no multi-user support. Not great.&lt;/p&gt;
&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;ChatGPT Web App&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex VSCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Works cleanly&lt;/td&gt;
&lt;td&gt;⚠️ Works with bugs&lt;/td&gt;
&lt;td&gt;❌ Not for custom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Respects scopes&lt;/td&gt;
&lt;td&gt;❌ Adds phantom&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redirect flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Proper&lt;/td&gt;
&lt;td&gt;⚠️ UI error (bug)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Rich annotations&lt;/td&gt;
&lt;td&gt;❌ Basic&lt;/td&gt;
&lt;td&gt;✅ Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom servers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full OAuth&lt;/td&gt;
&lt;td&gt;✅ Full OAuth&lt;/td&gt;
&lt;td&gt;❌ Bearer only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-tool auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ All-or-nothing&lt;/td&gt;
&lt;td&gt;❌ All-or-nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For OAuth2 testing today: ChatGPT Web App &amp;gt; Claude.ai/Desktop &amp;gt; Claude Code CLI &amp;gt; Codex VSCode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This hurts my feelings as a Claude fanboy (paying for Claude Pro Max 20x), but when OpenAI's ChatGPT Web App handles the OAuth2 specs better than Anthropic's own Claude Code — which literally references the MCP spec Anthropic wrote — something's off. The missing scope parameter bug (&lt;a href="https://github.com/anthropics/claude-code/issues/4540" rel="noopener noreferrer"&gt;#4540&lt;/a&gt;) has been open since July 2025, tagged &lt;code&gt;oncall&lt;/code&gt;, still unfixed. Meanwhile ChatGPT just... works.&lt;/p&gt;

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

&lt;p&gt;Still, I love Claude's tools and ecosystem — when they work!&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Official Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization Specification (2025-11-25)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ory.com/docs/hydra" rel="noopener noreferrer"&gt;Ory Hydra Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ory.com/docs/kratos" rel="noopener noreferrer"&gt;Ory Kratos Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  GitHub Repositories
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This guide's code&lt;/strong&gt;: &lt;a href="https://github.com/getlarge/claude-api-care-plugins/tree/main/plugins/baume/mcp-server" rel="noopener noreferrer"&gt;getlarge/claude-api-care-plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastify MCP plugin (OAuth2-ready)&lt;/strong&gt;: &lt;a href="https://github.com/getlarge/fastify-mcp" rel="noopener noreferrer"&gt;getlarge/fastify-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ory Hydra&lt;/strong&gt;: &lt;a href="https://github.com/ory/hydra" rel="noopener noreferrer"&gt;ory/hydra&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ory Kratos&lt;/strong&gt;: &lt;a href="https://github.com/ory/kratos" rel="noopener noreferrer"&gt;ory/kratos&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  RFCs and Standards Referenced
&lt;/h3&gt;

&lt;p&gt;
  Full list of referenced specifications
  &lt;ul&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13" rel="noopener noreferrer"&gt;OAuth 2.1 (draft-ietf-oauth-v2-1-13)&lt;/a&gt; - Core authorization framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc7591" rel="noopener noreferrer"&gt;RFC 7591 - Dynamic Client Registration&lt;/a&gt; - Runtime client registration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc8414" rel="noopener noreferrer"&gt;RFC 8414 - Authorization Server Metadata&lt;/a&gt; - AS endpoint discovery&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc8707" rel="noopener noreferrer"&gt;RFC 8707 - Resource Indicators&lt;/a&gt; - Token audience binding&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728 - Protected Resource Metadata&lt;/a&gt; - Resource server discovery&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00" rel="noopener noreferrer"&gt;OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-00)&lt;/a&gt; - CIMD&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;
&lt;h3&gt;
  
  
  Open Issues
&lt;/h3&gt;

&lt;p&gt;
  Issues I've encountered or created during this journey
  &lt;p&gt;&lt;strong&gt;Ory Hydra&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ory/hydra/discussions/2300" rel="noopener noreferrer"&gt;ory/hydra#2300&lt;/a&gt; - Partial scopes in consent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ory/hydra/issues/1618" rel="noopener noreferrer"&gt;ory/hydra#1618&lt;/a&gt; - Default scopes discussion&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ory/hydra/issues/4061" rel="noopener noreferrer"&gt;ory/hydra#4061&lt;/a&gt; - CIMD support request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ory Elements&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ory/elements/issues/587" rel="noopener noreferrer"&gt;ory/elements#587&lt;/a&gt; - Consent error despite successful flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Anthropic / Claude&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/4540" rel="noopener noreferrer"&gt;claude-code#4540&lt;/a&gt; - Missing scope parameter (main issue)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/7744" rel="noopener noreferrer"&gt;claude-code#7744&lt;/a&gt; - Related scopes_supported issue&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/13685" rel="noopener noreferrer"&gt;claude-code#13685&lt;/a&gt; - &lt;code&gt;client_uri&lt;/code&gt;, &lt;code&gt;logo_uri&lt;/code&gt;, &lt;code&gt;tos_uri&lt;/code&gt; must be parseable&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/2527" rel="noopener noreferrer"&gt;claude-code#2527&lt;/a&gt; - Azure AD/Entra ID integration complex due to DCR requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Platformatic MCP&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/platformatic/mcp/issues/95" rel="noopener noreferrer"&gt;platformatic/mcp#95&lt;/a&gt; - OAuth2 subscription handling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/platformatic/mcp/issues/96" rel="noopener noreferrer"&gt;platformatic/mcp#96&lt;/a&gt; - CORS configuration issues&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/platformatic/mcp/issues/99" rel="noopener noreferrer"&gt;platformatic/mcp#99&lt;/a&gt; - Authorization configuration limitations&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;


&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://isabellakohout.eu" rel="noopener noreferrer"&gt;&lt;strong&gt;Isabella Kohout&lt;/strong&gt;&lt;/a&gt; for the article cover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ory team&lt;/strong&gt; for building excellent open-source auth tooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic&lt;/strong&gt; for the MCP specification (and for eventually fixing the bugs 😅)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platformatic team&lt;/strong&gt; for the original MCP server implementation for Fastify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community&lt;/strong&gt; who reported issues and tested edge cases&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;em&gt;I spent a week debugging OAuth2 flows so you could skip that week. If it worked, pass it on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building something with this? Need help securing your MCP server? → &lt;a href="https://getlarge.eu" rel="noopener noreferrer"&gt;getlarge.eu&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1253749"&gt;
    &lt;a href="/getlarge" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="getlarge image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/getlarge"&gt;Edouard Maleix&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/getlarge"&gt;I am a Senior Software Engineer, focusing on distributed systems, application security and developer productivity/creativity.
Based in Vienna 🥐, Austria.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;






&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The MCP spec is a protocol revision maintained by the MCP project, not a ratified IETF standard — requirements may change between revisions. OAuth 2.1 itself is still an IETF draft. I reference the 2025-11-25 revision throughout this article. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>oauth2</category>
      <category>mcp</category>
      <category>claude</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>Ever felt like your authorization code could be easier to maintain and more flexible? How confident are you that only authorized users can access your API? When in doubt, have a look at OpenFGA! 👇</title>
      <dc:creator>Edouard Maleix</dc:creator>
      <pubDate>Wed, 18 Jun 2025 04:53:39 +0000</pubDate>
      <link>https://dev.to/getlarge/ever-felt-like-your-authorization-code-could-be-easier-to-maintain-and-more-flexible-how-4maa</link>
      <guid>https://dev.to/getlarge/ever-felt-like-your-authorization-code-could-be-easier-to-maintain-and-more-flexible-how-4maa</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/this-is-learning" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3314%2Fdc73eb74-08f9-4592-b599-c08f2bb14b4d.png" alt="This is Learning" width="192" height="192"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="" width="800" height="800"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/this-is-learning/how-to-protect-your-api-with-openfga-from-rebac-concepts-to-practical-usage-4n9j" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How to Protect Your API with OpenFGA: From ReBAC Concepts to Practical Usage&lt;/h2&gt;
      &lt;h3&gt;Edouard Maleix for This is Learning ・ Jun 15&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#tutorial&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#openfga&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#authorization&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#security&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>tutorial</category>
      <category>openfga</category>
      <category>authorization</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Protect Your API with OpenFGA: From ReBAC Concepts to Practical Usage</title>
      <dc:creator>Edouard Maleix</dc:creator>
      <pubDate>Sun, 15 Jun 2025 19:12:59 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/how-to-protect-your-api-with-openfga-from-rebac-concepts-to-practical-usage-4n9j</link>
      <guid>https://dev.to/playfulprogramming/how-to-protect-your-api-with-openfga-from-rebac-concepts-to-practical-usage-4n9j</guid>
      <description>&lt;p&gt;Another story, another article. A client asked me recently:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗣️ &lt;em&gt;Can we add temporary permissions for a group of users assigned to a maintenance task, while it's ongoing?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should be simple enough, right? Yes, until I examined the authorization code behind the API and found a &lt;strong&gt;500-line&lt;/strong&gt; function checking user roles and groups, time windows, resource ownership, and various business rules. 😶‍🌫️&lt;/p&gt;

&lt;p&gt;Unlike &lt;strong&gt;authentication&lt;/strong&gt; (who is accessing the system), where we have OIDC, JWT, and other established standards and patterns, &lt;strong&gt;authorization&lt;/strong&gt; (what they can do) often forces us into custom implementations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🫷&lt;em&gt;You might argue that OAuth 2.0 cover authorization, but they focus on third-party access, not complex and dynamic authorization patterns.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://owasp.org/API-Security/editions/2023/en/0x11-t10/" rel="noopener noreferrer"&gt;OWASP Top 10 API Security Risks&lt;/a&gt; lists &lt;strong&gt;Broken Object Level Authorization&lt;/strong&gt; as the #1 risk, showing us how common it is to expose sensitive data due to poor authorization checks.&lt;br&gt;
Is it far-fetched to think that the complexity of authorization logic contributes to this risk?&lt;/p&gt;

&lt;p&gt;Each new policy adds another &lt;strong&gt;conditional branch&lt;/strong&gt;, another &lt;strong&gt;database join&lt;/strong&gt;, another &lt;strong&gt;custom role&lt;/strong&gt;, another &lt;strong&gt;edge case that breaks&lt;/strong&gt; during the next feature request. The authorization flow becomes a spaghetti bowl and even experienced developers hesitate before touching it.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09cwef7zad5jqr7grjz7.png" alt="this is fine" width="500" height="771"&gt;


&lt;p&gt;Traditional approaches quickly hit walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RBAC&lt;/strong&gt; works until you need "sometimes" permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ABAC&lt;/strong&gt; offers flexibility but becomes a rule engine nightmare&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database queries&lt;/strong&gt; slow to a crawl as your permission matrix grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What if there is a better way? A way that lets you express complex relationships without messy code or performance hits? 🤔&lt;/p&gt;

&lt;p&gt;My exploration for a better paradigm started with &lt;a href="https://www.ory.sh/keto" rel="noopener noreferrer"&gt;&lt;strong&gt;Ory Keto&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/getlarge/integrate-ory-in-a-nestjs-application-4llo" class="crayons-story__hidden-navigation-link"&gt;Integrate Ory in a NestJS application&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/getlarge" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="getlarge profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/getlarge" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Edouard Maleix
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Edouard Maleix
                
              
              &lt;div id="story-author-preview-content-1823184" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/getlarge" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Edouard Maleix&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/getlarge/integrate-ory-in-a-nestjs-application-4llo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 16 '24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/getlarge/integrate-ory-in-a-nestjs-application-4llo" id="article-link-1823184"&gt;
          Integrate Ory in a NestJS application
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nestjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nestjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ory"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ory&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/getlarge/integrate-ory-in-a-nestjs-application-4llo" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/getlarge/integrate-ory-in-a-nestjs-application-4llo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            32 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;It introduced me to &lt;a href="https://storage.googleapis.com/gweb-research2023-media/pubtools/5068.pdf" rel="noopener noreferrer"&gt;Google's Zanzibar paper&lt;/a&gt; and the concept of &lt;strong&gt;Relation-Based Access Control (ReBAC)&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.zanzibar.academy/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.auth0.com%2Fwebsite%2Fzanzibar%2Fshare-asset.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.zanzibar.academy/" rel="noopener noreferrer" class="c-link"&gt;
            Zanzibar: A Global Authorization System - Presented by Auth0
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Zanzibar handles authorization for all of Google’s products, allows teams to specify their unique authorization models, globally replicates authorization data, and responds to access checks blazing fast.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.auth0.com%2Fwebsite%2Fzanzibar%2Ffavicon-v2.png" width="32" height="32"&gt;
          zanzibar.academy
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;This time, I needed more flexibility to introduce &lt;strong&gt;contextual relationships&lt;/strong&gt;. That "simple" feature request led me to &lt;a href="https://openfga.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenFGA&lt;/strong&gt;&lt;/a&gt; — a richer implementation of Zanzibar's principles that extends ReBAC with powerful features like contextual-based conditions, attribute-based access, and a simple query language.&lt;/p&gt;

&lt;p&gt;And since you might be familiar with this story, I'll share with you my &lt;strong&gt;learning journey&lt;/strong&gt;, starting from the concepts and terminology, through practical examples and considerations, to real-world usage of OpenFGA.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ The Authorization Problem&lt;/li&gt;
&lt;li&gt;📍 &lt;strong&gt;Why OpenFGA?&lt;/strong&gt; ← You are here&lt;/li&gt;
&lt;li&gt;⬜ ReBAC and OpenFGA concepts
&lt;/li&gt;
&lt;li&gt;⬜ OpenFGA in Action
&lt;/li&gt;
&lt;li&gt;⬜ Testing permissions with OpenFGA CLI
&lt;/li&gt;
&lt;li&gt;⬜ Adoption Challenges and Strategies
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;a id="why-openfga"&gt;&lt;/a&gt; Why OpenFGA [▓░░░░░░░]
&lt;/h2&gt;

&lt;p&gt;Before I grab your attention and your brain 🧠 with the ReBAC concepts and how OpenFGA implements them, let me explain why I chose OpenFGA over other solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  It Matches How You Think
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Expressive Relationships
&lt;/h4&gt;

&lt;p&gt;Cat owners own cats. Sitters sit cats. Admins administrate. The authorization model mirrors reality instead of forcing you into artificial role hierarchies. Demo&lt;/p&gt;

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

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

&lt;h4&gt;
  
  
  Time Works Automatically
&lt;/h4&gt;

&lt;p&gt;No more "grant permission at 9 AM, revoke at 5 PM" cron jobs. Time-based access happens naturally through conditions.&lt;br&gt;
Grant permissions only when conditions are met—like during scheduled hours.&lt;br&gt;
Demo&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤝 &lt;em&gt;Yes! My client is going to love this.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Your app's workflow probably includes some entities' states (e.g., pending, active, completed). OpenFGA uses these attributes directly for permissions instead of requiring separate access control flags. Demo&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcsgc6v4mvlk58cnvxip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcsgc6v4mvlk58cnvxip.png" alt="Status-based conditions" width="800" height="1262"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Queries, Not Just Checks
&lt;/h3&gt;

&lt;p&gt;Traditional systems answer "Can Alice do X?" OpenFGA also answers "What can Alice do?" and "Who can do X?" This opens opportunities for features like smart dashboards and permission audits.&lt;br&gt;
Demo&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gljlmtefso79v0rdas9.png" alt="Is user Jenny related to system development as an admin?" width="800" height="691"&gt;Tuple Queries from OpenFGA playground
  



  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmmkh9zd1uw4igfawse7.png" alt="Who is Romeo's owner?" width="800" height="693"&gt;Who has the right to feed Romeo?
  

&lt;h3&gt;
  
  
  Scale Like Google
&lt;/h3&gt;

&lt;p&gt;Google's &lt;a href="https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/" rel="noopener noreferrer"&gt;Zanzibar&lt;/a&gt; (which inspired OpenFGA) handles &lt;strong&gt;billions&lt;/strong&gt; of authorization checks daily. Your application(s) probably won't hit those numbers, but it's nice to know you won't hit a wall due to a poorly performing authorization system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;☝️ &lt;em&gt;In my &lt;a href="https://github.com/getlarge/purrfect-sitter/blob/main/tools/scripts/benchmark-auth-strategies.ts" rel="noopener noreferrer"&gt;tests&lt;/a&gt;, OpenFGA performed slightly better than custom database lookups for complex relationships (both based on PostgreSQL).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Great Documentation and CLI Tools
&lt;/h3&gt;

&lt;p&gt;I can't deny it, OpenFGA has a steep learning curve, but its &lt;a href="https://openfga.dev/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; is complete and well-structured. It covers everything from basic concepts to advanced usage patterns until deployment strategies.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://openfga.dev/docs/cli" rel="noopener noreferrer"&gt;CLI tools&lt;/a&gt; make it easy to manage your authorization model and test your policies.&lt;/p&gt;
&lt;h3&gt;
  
  
  Business Backing
&lt;/h3&gt;

&lt;p&gt;OpenFGA is open-source and Okta is funding it, this ensures a long-term viability and support. On one side, you can always deploy it on your own infrastructure, and the community is growing rapidly. On the other side, Okta has strong competitors like OSO, Ory Keto or AWS Cedar, so they have a vested interest in making OpenFGA a successful product extending what Auth0 has to offer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Deployment Flexibility
&lt;/h3&gt;

&lt;p&gt;OpenFGA can be deployed in various ways, depending on your needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt;: Use &lt;a href="https://openfga.dev/docs/getting-started/setup-openfga/docker" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; to get started locally, &lt;a href="https://artifacthub.io/packages/helm/openfga/openfga" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; for full control and &lt;a href="https://registry.terraform.io/providers/openfga/openfga/latest/docs" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; to orchestrate your infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed service&lt;/strong&gt;: Use Auth0's FGA offering for a hassle-free experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Observability and Debugging
&lt;/h3&gt;

&lt;p&gt;You can configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; for traces collection on the &lt;a href="https://openfga.dev/docs/getting-started/configure-telemetry#enabling-telemetry" rel="noopener noreferrer"&gt;client&lt;/a&gt; and the &lt;a href="https://openfga.dev/docs/getting-started/setup-openfga/configure-openfga#tracing" rel="noopener noreferrer"&gt;server&lt;/a&gt; side.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://prometheus.io/docs/concepts/data_model/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; for metrics collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easier to monitor your authorization system with open standards and integrate with your existing observability stack.&lt;/p&gt;
&lt;h3&gt;
  
  
  Simpler Code To Maintain
&lt;/h3&gt;

&lt;p&gt;To illustrate this, I'm using Typescript to check if a user can update a cat sitting arrangement with both approaches: a plain &lt;strong&gt;database lookup&lt;/strong&gt; and an &lt;strong&gt;OpenFGA check&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Database Lookup
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSystemAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkCatSittingUpdatePermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sittingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sitting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;catSittingRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sittingId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sitting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;catRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;catId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isOwner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ownerId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sitting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sitterId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isSystemAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;sitting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requested&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isOwner&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isSitter&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  OpenFGA Check
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkCatSittingUpdatePermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;catSittingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openfgaClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenFgaApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENFGA_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tuple_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;can_update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`cat_sitting:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;catSittingId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openfgaClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FGA_STORE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Does it need a lot of explanation? The OpenFGA version is objectively cleaner, more maintainable, and scales better as your authorization logic grows.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;a id="rebac-and-openfga-concepts"&gt;&lt;/a&gt; ReBAC and OpenFGA concepts [▓▓▓░░░░]
&lt;/h2&gt;

&lt;p&gt;I'll walk you through ReBAC using PurrfectSitter ©, a cat sitting app where owners find sitters. Real problems, real solutions.&lt;br&gt;
As trivial as it sounds, this example shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role-based access control (RBAC) for admins&lt;/li&gt;
&lt;li&gt;Attribute-based access control (status-driven permissions)&lt;/li&gt;
&lt;li&gt;Time-based access control&lt;/li&gt;
&lt;li&gt;Resource ownership and management&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Three Building Blocks
&lt;/h3&gt;

&lt;p&gt;ReBAC builds authorization from three simple pieces:&lt;/p&gt;
&lt;h4&gt;
  
  
  Types: Entities in Your App
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;type user&lt;/span&gt;
&lt;span class="s"&gt;type system&lt;/span&gt;
&lt;span class="s"&gt;type cat&lt;/span&gt;
&lt;span class="s"&gt;type cat_sitting&lt;/span&gt;
&lt;span class="s"&gt;type review&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These map to your app's core entities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt;: 👤 People using your app&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;system&lt;/code&gt;: 🏢 Admin access controls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat&lt;/code&gt;: 🐱 Furry clients needing care&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting&lt;/code&gt;: 🏠 A sitting arrangement&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;review&lt;/code&gt;: 📝 Post-sitting feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each &lt;strong&gt;type&lt;/strong&gt; will declare relationships with other types - in the &lt;strong&gt;type definition&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;In Ory Keto, these are called &lt;strong&gt;namespaces&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Objects: Instances of Types
&lt;/h4&gt;

&lt;p&gt;In OpenFGA, an &lt;strong&gt;object&lt;/strong&gt; is an instance of a type. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user:bob&lt;/code&gt;: A specific user named Bob&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat:romeo&lt;/code&gt;: A specific cat named Romeo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;system:development&lt;/code&gt;: The development environment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting:1&lt;/code&gt;: The first cat sitting arrangement&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;review:1&lt;/code&gt;: The first review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Objects are the concrete entities your users interact with.&lt;/p&gt;
&lt;h4&gt;
  
  
  Users: The Actors
&lt;/h4&gt;

&lt;p&gt;A &lt;strong&gt;user&lt;/strong&gt; is an entity that is related to objects in your system. In our app, users can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People (like Bob)&lt;/li&gt;
&lt;li&gt;Systems (like the PurrfectSitter development environment)&lt;/li&gt;
&lt;li&gt;Cats (like Romeo)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;In Ory Keto, these are called &lt;strong&gt;subjects&lt;/strong&gt;. I believe subject is less ambiguous than user, but OpenFGA uses user, so we will too.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Relations: How Things Connect
&lt;/h4&gt;

&lt;p&gt;A &lt;strong&gt;relation&lt;/strong&gt; defines how users interact with objects. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user:bob owner cat:romeo&lt;/code&gt;: Bob is the owner of Romeo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user:anne sitter cat_sitting:1&lt;/code&gt;: Anne is the sitter for the first cat sitting arrangement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each &lt;strong&gt;relation&lt;/strong&gt; (e.g., &lt;code&gt;admin&lt;/code&gt;) evaluation logic is defined in the &lt;strong&gt;relation definition&lt;/strong&gt; (e.g., &lt;code&gt;[user]&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;type system&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define admin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;type cat&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define owner&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin from system&lt;/span&gt;
    &lt;span class="na"&gt;define can_manage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner or admin&lt;/span&gt;
    &lt;span class="na"&gt;define system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;type cat_sitting&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define active_sitter&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat_sitting#sitter with is_active_timeslot&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define can_post_updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner or active_sitter&lt;/span&gt;
    &lt;span class="na"&gt;define can_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat#owner with is_cat_sitting_completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define cat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner from cat&lt;/span&gt;
    &lt;span class="na"&gt;define sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;‼️ &lt;em&gt;For the sake of this example, we will assume that cats are owned by humans. We all know that, in reality, cats own us, not the other way around.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenFGA computes relationships in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;system.admin&lt;/code&gt; — A &lt;em&gt;user&lt;/em&gt; can be an &lt;strong&gt;admin&lt;/strong&gt; of the &lt;em&gt;system&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat.owner&lt;/code&gt; — A &lt;em&gt;user&lt;/em&gt; can be a &lt;em&gt;cat&lt;/em&gt; &lt;strong&gt;owner&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat.system&lt;/code&gt; — A &lt;em&gt;system&lt;/em&gt; can be assigned to a &lt;em&gt;cat&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting.sitter&lt;/code&gt; — A &lt;em&gt;user&lt;/em&gt; can be a &lt;strong&gt;sitter&lt;/strong&gt; for a &lt;em&gt;cat_sitting&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implied&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cat.admin: admin from system&lt;/code&gt; — An &lt;strong&gt;admin&lt;/strong&gt; is a &lt;em&gt;user&lt;/em&gt; from the &lt;em&gt;system&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting.owner: owner from cat&lt;/code&gt; — The &lt;em&gt;cat_sitting&lt;/em&gt; &lt;strong&gt;owner&lt;/strong&gt; is the &lt;em&gt;cat&lt;/em&gt; &lt;strong&gt;owner&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Union&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cat.can_manage&lt;/code&gt; — either the &lt;em&gt;cat&lt;/em&gt; &lt;strong&gt;owner&lt;/strong&gt; or an &lt;em&gt;admin&lt;/em&gt; from the &lt;em&gt;system&lt;/em&gt; can manage the &lt;em&gt;cat&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting.can_post_updates&lt;/code&gt; — either the &lt;em&gt;cat_sitting&lt;/em&gt; &lt;strong&gt;owner&lt;/strong&gt; or an &lt;em&gt;active_sitter&lt;/em&gt; can post updates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting.active_sitter&lt;/code&gt; — Conditional relation between &lt;em&gt;user&lt;/em&gt; and &lt;em&gt;cat_sitting&lt;/em&gt; based on the outcome of the &lt;code&gt;is_active_timeslot&lt;/code&gt; condition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat_sitting.can_review&lt;/code&gt; — Conditional relation between &lt;em&gt;user&lt;/em&gt; and &lt;em&gt;cat_sitting&lt;/em&gt; based on the outcome of the &lt;code&gt;is_cat_sitting_completed&lt;/code&gt; condition&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are even more ways to express relationships, such as &lt;strong&gt;exclusion&lt;/strong&gt;, &lt;strong&gt;intersection&lt;/strong&gt; and &lt;strong&gt;nesting&lt;/strong&gt;, you can find the complete &lt;strong&gt;configuration language&lt;/strong&gt; reference in the &lt;a href="https://openfga.dev/docs/configuration-language" rel="noopener noreferrer"&gt;OpenFGA documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Complete Authorization Model
&lt;/h3&gt;

&lt;p&gt;The ensemble of &lt;strong&gt;types&lt;/strong&gt; and &lt;strong&gt;relations&lt;/strong&gt; definitions forms the &lt;strong&gt;authorization model&lt;/strong&gt;.&lt;br&gt;
Here, the PurrfectSitter's authorization model in OpenFGA's configuration language (&lt;strong&gt;D&lt;/strong&gt;omain-&lt;strong&gt;S&lt;/strong&gt;pecific &lt;strong&gt;L&lt;/strong&gt;anguage for the purist), defines how &lt;strong&gt;users&lt;/strong&gt; interact with &lt;strong&gt;cats&lt;/strong&gt;, &lt;strong&gt;cat sittings&lt;/strong&gt;, and &lt;strong&gt;reviews&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;model&lt;/span&gt;
  &lt;span class="s"&gt;schema &lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;

&lt;span class="s"&gt;type user&lt;/span&gt;

&lt;span class="s"&gt;type system&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define admin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;type cat&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define admin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin from system&lt;/span&gt;
    &lt;span class="s"&gt;define can_manage&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner or admin&lt;/span&gt;
    &lt;span class="s"&gt;define owner&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;type cat_sitting&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define admin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin from system&lt;/span&gt;
    &lt;span class="s"&gt;define active_sitter&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat_sitting#sitter with is_active_timeslot&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define pending_sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat_sitting#sitter with is_pending_timeslot&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define can_post_updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner or active_sitter&lt;/span&gt;
    &lt;span class="na"&gt;define can_delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin or owner or pending_sitter&lt;/span&gt;
    &lt;span class="na"&gt;define can_view&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin or owner or sitter&lt;/span&gt;
    &lt;span class="na"&gt;define can_update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin or owner or pending_sitter&lt;/span&gt;
    &lt;span class="na"&gt;define can_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat#owner with is_cat_sitting_completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define cat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner from cat&lt;/span&gt;
    &lt;span class="na"&gt;define sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;type review&lt;/span&gt;
  &lt;span class="s"&gt;relations&lt;/span&gt;
    &lt;span class="s"&gt;define admin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin from system&lt;/span&gt;
    &lt;span class="s"&gt;define author&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner from cat_sitting&lt;/span&gt;
    &lt;span class="s"&gt;define can_delete&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin or author&lt;/span&gt;
    &lt;span class="s"&gt;define can_edit&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin or author&lt;/span&gt;
    &lt;span class="s"&gt;define can_view&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define cat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat from cat_sitting&lt;/span&gt;
    &lt;span class="na"&gt;define cat_sitting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cat_sitting&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;define subject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sitter from cat_sitting&lt;/span&gt;
    &lt;span class="na"&gt;define system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;condition is_active_timeslot(current_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp, end_time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp, start_time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp) {&lt;/span&gt;
  &lt;span class="s"&gt;current_time &amp;gt;= start_time &amp;amp;&amp;amp; current_time &amp;lt;= end_time&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;condition is_pending_timeslot(current_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp, start_time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp) {&lt;/span&gt;
  &lt;span class="s"&gt;current_time &amp;lt; start_time&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;condition is_cat_sitting_completed(cat_sitting_attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;map&amp;lt;string&amp;gt;, completed_statuses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;list&amp;lt;string&amp;gt;) {&lt;/span&gt;
  &lt;span class="s"&gt;cat_sitting_attributes["status"] in completed_statuses&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice how readable, yet compact, this is — no complex SQL joins or nested conditions. The model captures business logic naturally.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pr0ewnbahfpu06fc8ee.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pr0ewnbahfpu06fc8ee.gif" alt="Nice one Johnny" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  ✅ Checkpoint: Can You Answer These?
&lt;/h2&gt;

&lt;p&gt;Before moving on, make sure you can answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What's the difference between a user and an object?&lt;/li&gt;
&lt;li&gt;How do relations differ from roles?&lt;/li&gt;
&lt;li&gt;When would you use indirect relationships?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
  &lt;strong&gt;Answers&lt;/strong&gt;
  &lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User vs Object&lt;/strong&gt;: A user is an entity (like a person), while an object is an instance of a type (like a specific cat or cat sitting arrangement). Users interact with objects through relations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relations vs Roles&lt;/strong&gt;: Relations define how entities connect (like "owner of cat"), while roles are broader categories (like "admin" or "sitter") that can have multiple relations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indirect Relationships&lt;/strong&gt;: Use these when you want to derive permissions from other relationships, like "can a sitter post updates if they are also the owner?" This allows for more flexible and dynamic permission checks.&lt;/li&gt;
&lt;/ol&gt;



&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;a id="openfga-in-action"&gt;&lt;/a&gt; OpenFGA in Action [▓▓▓░░░░]
&lt;/h2&gt;

&lt;p&gt;Let's test our model with real scenarios. I use the OpenFGA CLI to initialize the authorization model, create relation tuples, check the permissions and run some querie but you can use any &lt;a href="https://openfga.dev/docs/getting-started/install-sdk" rel="noopener noreferrer"&gt;other client SDK&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://github.com/codespaces/new?template_repository=getlarge/purrfect-sitter" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Save some time, create a GitHub codespace&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;It will provide you a ready-to-use environment with all dependencies installed and external services running, so you can focus on running the examples in this article.&lt;/p&gt;


&lt;h3&gt;
  
  
  Setup OpenFGA
&lt;/h3&gt;
&lt;h4&gt;
  
  
  &lt;a id="creating-a-store-and-a-model"&gt;&lt;/a&gt; 1. Creating a Store and a Model
&lt;/h4&gt;

&lt;p&gt;First, create a store:&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;



&lt;p&gt;Then create the authorization model in the new store:&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;em&gt;If you are using Codespaces, specify the API path with&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;the flag &lt;code&gt;--api-url http://openfga:8080&lt;/code&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;the environment variable &lt;code&gt;FGA_API_URL=http://openfga:8080&lt;/code&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  &lt;a id="create-basic-relationships"&gt;&lt;/a&gt; 2. Creating Basic Relationships
&lt;/h4&gt;

&lt;p&gt;Bob owns Romeo, Anne sits for him. Simple.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h4&gt;
  
  
  &lt;a id="create-admins"&gt;&lt;/a&gt; 3. Admin Powers
&lt;/h4&gt;

&lt;p&gt;Jenny becomes a system admin who can manage any cat — traditional RBAC within ReBAC.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h4&gt;
  
  
  &lt;a id="time-based-conditions"&gt;&lt;/a&gt; 4. Time Magic
&lt;/h4&gt;

&lt;p&gt;Anne's permissions activate and deactivate automatically based on time. No cron jobs, no cleanup code — the authorization system handles it.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h4&gt;
  
  
  &lt;a id="state-based-conditions"&gt;&lt;/a&gt; 5. Status-Driven Access
&lt;/h4&gt;

&lt;p&gt;Reviews only make sense after sitting ends. OpenFGA enforces this business rule automatically, ABAC style.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h4&gt;
  
  
  &lt;a id="check-permissions-and-query-relations"&gt;&lt;/a&gt; 6. Creating and Checking Review Permissions
&lt;/h4&gt;

&lt;p&gt;Create a review and check who can edit or delete it. OpenFGA's query language shines here, allowing you to check permissions and also list objects a user can interact with.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h4&gt;
  
  
  &lt;a id="making-an-object-public"&gt;&lt;/a&gt; 7. Making the Review Public
&lt;/h4&gt;

&lt;p&gt;Control visibility using wildcards.&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


&lt;h3&gt;
  
  
  Explore Relationships with OpenFGA Playground
&lt;/h3&gt;

&lt;p&gt;You can visualize the relations graph and run queries in the &lt;a href="https://openfga.dev/docs/getting-started/setup-openfga/playground" rel="noopener noreferrer"&gt;OpenFGA's Playground&lt;/a&gt;.&lt;br&gt;
I find it a great way to discover and understand relationships in your model and test queries interactively.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;em&gt;If you are using Codespaces, just open &lt;code&gt;http://localhost:8082/playground&lt;/code&gt; in your browser.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  &lt;a id="testing-permissions-with-openfga-cli"&gt;&lt;/a&gt; Testing permissions with OpenFGA CLI [▓▓▓▓░░░]
&lt;/h2&gt;

&lt;p&gt;Another one of OpenFGA's strengths, is its built-in testing capabilities. The CLI provides a declarative way to test authorization models without writing application code.&lt;/p&gt;


&lt;h3&gt;
  
  
  Declarative Testing with YAML
&lt;/h3&gt;

&lt;p&gt;Define tests in YAML and run with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fga model &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--tests&lt;/span&gt; store.fga.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;...and forget about all the commands above 🙂. The &lt;code&gt;store.fga.yml&lt;/code&gt; file contains everything you need to create the model and tuples, and run the tests before writing application code!&lt;/p&gt;

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

&lt;p&gt;Let's look at the &lt;code&gt;store.fga.yml&lt;/code&gt; file that tests our PurrfectSitter model:&lt;/p&gt;
&lt;h4&gt;
  
  
  The authorization model
&lt;/h4&gt;

&lt;p&gt;This is the model we defined earlier, but in YAML format for the OpenFGA CLI:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;model&lt;/span&gt;
    &lt;span class="s"&gt;schema 1.1&lt;/span&gt;

  &lt;span class="s"&gt;# Our full model definition goes here...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;The model section is the same as the one we defined earlier, but in YAML format for the OpenFGA CLI.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  The tuples
&lt;/h4&gt;

&lt;p&gt;This section defines the relationships (tuples) in our model. Each tuple represents a relationship between a user and an object, along with the relation type.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="na"&gt;tuples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:jenny&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system:development&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system:development&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sitter&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1#sitter&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active_sitter&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
    &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_active_timeslot&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;start_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-01-01T00:00:00Z'&lt;/span&gt;
        &lt;span class="na"&gt;end_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-01-02T00:00:00Z'&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo#owner&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;can_review&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
    &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_cat_sitting_completed&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;completed_statuses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;completed'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system:development&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:*&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;can_view&lt;/span&gt;
    &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review:1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  The tests
&lt;/h4&gt;

&lt;p&gt;This section defines the tests that will be run against the model and tuples. Each test checks specific permissions or relationships.&lt;/p&gt;

&lt;p&gt;The example demonstrates several test types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic permission checks&lt;/strong&gt;: Simple assertions about relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual checks&lt;/strong&gt;: Testing time-based permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribute-based checks&lt;/strong&gt;: Testing permissions depending on object attributes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List objects&lt;/strong&gt;: Finding objects a user has relationships with&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List users&lt;/strong&gt;: Finding users with relationships to an object
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="na"&gt;tuples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test basic relations&lt;/span&gt;
    &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test role access&lt;/span&gt;
    &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:jenny&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_manage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_manage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat:romeo&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_manage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test temporal access&lt;/span&gt;
    &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;current_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-01-01T00:10:00Z'&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;active_sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;current_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-01-04T00:00:00Z'&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;active_sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test attribute access&lt;/span&gt;
    &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cat_sitting_attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;completed'&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cat_sitting_attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;in_progress'&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test the cat sitting that anne is sitting&lt;/span&gt;
    &lt;span class="na"&gt;list_objects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:anne&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat_sitting&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;current_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-01-01T00:00:01Z'&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;active_sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;
          &lt;span class="na"&gt;sitter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat_sitting:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test the review that bob can edit&lt;/span&gt;
    &lt;span class="na"&gt;list_objects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user:bob&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_edit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;review:1&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test that reviews are public&lt;/span&gt;
    &lt;span class="na"&gt;list_users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review:1&lt;/span&gt;
        &lt;span class="na"&gt;user_filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
        &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;can_view&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;user:*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;👋 &lt;em&gt;You can find &lt;code&gt;store.fga.yml&lt;/code&gt; in the &lt;a href="https://github.com/getlarge/purrfect-sitter/blob/main/store.fga.yml" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing During Adoption
&lt;/h3&gt;

&lt;p&gt;These testing capabilities help when adopting OpenFGA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate models against business rules&lt;/li&gt;
&lt;li&gt;Verify permissions match the old system during migration&lt;/li&gt;
&lt;li&gt;Compare results with your existing system in shadow mode&lt;/li&gt;
&lt;li&gt;Prevent regressions with CI pipeline tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Including tests in your workflow reduces authorization errors and builds confidence in your implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Checkpoint II: Can You Answer These?
&lt;/h2&gt;

&lt;p&gt;Have you read carefully the previous sections? If so, you should be able to answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can you list objects a user has relationships with?&lt;/li&gt;
&lt;li&gt;Can you list users with relationships to an object?&lt;/li&gt;
&lt;li&gt;Can you make an object public to all users?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
  &lt;strong&gt;Answers&lt;/strong&gt;
  &lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;List Objects&lt;/strong&gt;: Yes, you can use the &lt;a href="https://openfga.dev/docs/getting-started/perform-list-objects" rel="noopener noreferrer"&gt;list-objects&lt;/a&gt; command to find objects a user has relationships with, like finding all cat sittings where a user is an active sitter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;List Users&lt;/strong&gt;: Yes, if you have a relationship that connects users to objects, you can use the &lt;a href="https://openfga.dev/docs/getting-started/perform-list-users" rel="noopener noreferrer"&gt;list-users&lt;/a&gt; command to find users with relationships to an object, like finding all users who can view a specific review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Public Objects&lt;/strong&gt;: Make an object public by adding a relation that allows all users to access it, like granting &lt;code&gt;user:*&lt;/code&gt; permission on the object as I showed you in this example&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;



&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a id="adoption-challenges-and-strategies"&gt;&lt;/a&gt; Adoption Challenges and Strategies [▓▓▓▓▓▓░]
&lt;/h2&gt;

&lt;p&gt;As good as this tool is, adopting OpenFGA in existing systems presents challenges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mental Model Shift
&lt;/h3&gt;

&lt;p&gt;ReBAC requires a paradigm shift for developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mental model adjustment&lt;/strong&gt;: Developers familiar with RBAC or ABAC need time to think in relationships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training investment&lt;/strong&gt;: Workshops and examples help teams translate existing rules into relationship models.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data Synchronization
&lt;/h3&gt;

&lt;p&gt;This is probably the most challenging aspect of adopting OpenFGA, especially if you have an existing database with complex permissions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dual writes&lt;/strong&gt;: Applications must write to both their database and OpenFGA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronization strategies&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Event-driven synchronization through message queues&lt;/li&gt;
&lt;li&gt;Centralized hooks for database operations&lt;/li&gt;
&lt;li&gt;Transactional outbox pattern for consistency&lt;/li&gt;
&lt;li&gt;Background jobs for existing data&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Read this excellent article 👇 about dual writes in distributed systems. It will surely help you understand strategies for synchronizing data between your applications' DB and OpenFGA.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://auth0.com/blog/handling-the-dual-write-problem-in-distributed-systems/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F7opCVXMvjs6y4PKe74c34X%2Fd9a432edd8b4c5252dcf73c52fcbdb93%2FPasswordless-Authentication-hero.jpg" height="718" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://auth0.com/blog/handling-the-dual-write-problem-in-distributed-systems/" rel="noopener noreferrer" class="c-link"&gt;
            Handling the Dual-Write Problem in Distributed Systems | Auth0
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            The dual-write problem exists in distributed systems. Let's look at what it is and some of the most common strategies to mitigate it usin...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.auth0.com%2Fwebsite%2Fwebsite%2Ffavicons%2Fauth0-favicon.svg" width="32" height="32"&gt;
          auth0.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  Progressive Adoption
&lt;/h3&gt;

&lt;p&gt;It's going to be hard (and unwise) to convince your team to rewrite the entire authorization logic in OpenFGA, big-bang refactoring style. Instead, consider a &lt;strong&gt;progressive adoption&lt;/strong&gt; strategy:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Start with Coarse-Grained Permissions
&lt;/h4&gt;

&lt;p&gt;Begin with your existing structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replicate your current RBAC model&lt;/li&gt;
&lt;li&gt;Add organization-level permissions&lt;/li&gt;
&lt;li&gt;Gradually introduce finer-grained controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Shadow Mode Implementation
&lt;/h4&gt;

&lt;p&gt;Before switching fully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run existing authorization alongside OpenFGA&lt;/li&gt;
&lt;li&gt;Compare results to identify discrepancies&lt;/li&gt;
&lt;li&gt;Build confidence before making the switch&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Use Contextual Tuples for Hybrid Implementations
&lt;/h4&gt;

&lt;p&gt;Reduce synchronization burden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send data as contextual tuples initially&lt;/li&gt;
&lt;li&gt;Gradually move to persistent relationship tuples&lt;/li&gt;
&lt;li&gt;Use contextual tuples for frequently changing data&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;Read more about this technique in the &lt;a href="https://openfga.dev/docs/best-practices/adoption-patterns#provide-request-level-data" rel="noopener noreferrer"&gt;OpenFGA documentation&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Managing Organizational Adoption
&lt;/h3&gt;

&lt;p&gt;For large organizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a single application where OpenFGA delivers immediate value&lt;/li&gt;
&lt;li&gt;Use modular models for independent team control&lt;/li&gt;
&lt;li&gt;Leverage access control for team-specific credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a id="your-next-move"&gt;&lt;/a&gt; Your Next Move [▓▓▓▓▓▓▓]
&lt;/h2&gt;

&lt;p&gt;Complex policies doesn't have to mean complex code. OpenFGA's ReBAC model simplifies permissions into relationships, making your authorization logic more maintainable and scalable.&lt;/p&gt;

&lt;p&gt;Ready to get started? Here's your roadmap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read in details the &lt;a href="https://github.com/getlarge/purrfect-sitter/blob/main/purrfect-sitter-model.fga" rel="noopener noreferrer"&gt;PurrfectSitter's authorization model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Draw inspiration from the &lt;a href="https://github.com/getlarge/purrfect-sitter/blob/main/apps/purrfect-sitter/src/main.ts" rel="noopener noreferrer"&gt;Purrfect Sitter Fastify API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Adapt it to your domain&lt;/li&gt;
&lt;li&gt;Watch complex permission logic become simple relationship definitions.&lt;/li&gt;
&lt;li&gt;Get in touch with me for some deep-dive into performance characteristics, monitoring, and production deployment patterns 😉&lt;/li&gt;
&lt;li&gt;If you want to show your appreciation, give those repositories a ⭐️ on GitHub. 👇&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/getlarge" rel="noopener noreferrer"&gt;
        getlarge
      &lt;/a&gt; / &lt;a href="https://github.com/getlarge/purrfect-sitter" rel="noopener noreferrer"&gt;
        purrfect-sitter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A cat sitting management application to showcase OpenFGA
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Purrfect Sitter&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A cat sitting application with dual authorization strategies
This is the support project for the &lt;a href="https://dev.to/this-is-learning/how-to-protect-your-api-with-openfga-from-rebac-concepts-to-practical-usage-4n9j" rel="nofollow"&gt;How to Protect Your API with OpenFGA&lt;/a&gt; article.&lt;/p&gt;
  &lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8c64eba4153dba5f2c3b33d76ce10405802734e5c19069d2ca74a477b1f52300/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f66616f79626f6a3469686a726e336d73796e63342e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/8c64eba4153dba5f2c3b33d76ce10405802734e5c19069d2ca74a477b1f52300/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f66616f79626f6a3469686a726e336d73796e63342e706e67" alt="An anonymous developer surrounded by cats"&gt;&lt;/a&gt;&lt;/p&gt;
  An anonymous developer surrounded by cats

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Core Application&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Fastify-based API&lt;/li&gt;
&lt;li&gt;User authentication with Ory Kratos&lt;/li&gt;
&lt;li&gt;Core models: User, Cat, CatSitting, Review&lt;/li&gt;
&lt;li&gt;TypeBox for JSON Schema validation&lt;/li&gt;
&lt;li&gt;Drizzle ORM for database access&lt;/li&gt;
&lt;li&gt;OpenFGA for fine-grained authorization&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;NX Workspace Structure&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;apps/
  purrfect-sitter/       # Main Fastify application
  purrfect-sitter-e2e/   # End-to-end tests

libs/                    # Domain-specific libraries
  database/              # Database schema and repositories
  models/                # DTOs and validation schemas
  auth/                  # Authentication and authorization
  cats/                  # Cats domain business logic
  cat-sittings/          # Cat sittings domain business logic
  reviews/               # Reviews domain business logic
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Authorization Strategies&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;This application implements two authorization strategies that can be toggled via environment variables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Database Lookups&lt;/strong&gt; (&lt;code&gt;AUTH_STRATEGY=db&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Traditional database queries to check permissions&lt;/li&gt;
&lt;li&gt;Uses JOINs and WHERE clauses for relationship checks&lt;/li&gt;
&lt;li&gt;Direct SQL…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/getlarge/purrfect-sitter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/openfga" rel="noopener noreferrer"&gt;
        openfga
      &lt;/a&gt; / &lt;a href="https://github.com/openfga/openfga" rel="noopener noreferrer"&gt;
        openfga
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/openfga/openfga/./openfga-logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fopenfga%2Fopenfga%2FHEAD%2F.%2Fopenfga-logo.png" alt="OpenFGA Logo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OpenFGA&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://openfga.dev/community" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1af28927cb33d2f059334afe01927fc1085e1b381c44f63c02fe35dc07d7ef41/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f736c61636b2d636e63665f2532336f70656e6667612d3430616262382e7376673f6c6f676f3d736c61636b" alt="Join our community"&gt;&lt;/a&gt;
&lt;a href="https://deepwiki.com/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/86bbe538660d18e0c28f8761215a0bee9d1bfc846c4ae9e4769db41d1a82450f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4465657057696b692d6f70656e6667612532466f70656e6667612d626c75652e7376673f6c6f676f3d646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e53556845556741414143774141414179434159414141416e57446e714141414141584e535230494172733463365141414130354a52454655614550746d557479457a4551687457545179514c484e616b324142375a6e79585a4d456a584d47654b2f4149692b517548724d6e6243685959374d496838673031664a6f6f70466230756868457171636257547030362f7576317361454476344f336e33645636305266503934374d6d392f5351633049434651677a66633443595a6f545041737767534a4343554a556e41416f52484f41554f63415477626d564c5764476f482f2f5042386d6e4b71536341687344306b5950336a2f5974354c505165324b7663586d4776524863446e7078664c327a4f594a316d467772727957547a306164767631557434434a67663575684475446a3565556341556f61687264592f353665625257657261546a4d742f30305368335544746a674874514e48776352474f433938424a454145796d79636d596357774f70725467634236565a354a4b3554414a2b6658474c426d334644416d6e366f50506a5234724b43416f4a43616c326541695170327830767854504233414c4f3243526b776d447935576f687a4244775345464b527750626b6e4567674350422f696d77727963677858324e7a6f4d434868506b447771594d72397452635035714e724d5a486b566e4f6a524d57774c436372386f68425662314f4d6a784c774743766a54696b7273424f694136664e7943726d38563172503933695650707761452b674f305373576d506958422b6a696b64663653697a725435714b617378356a3841426248704654782b7646587039456e59516d4c7830326831515454726c36654471784c6e476a706f72786c334e4c336167457658645430576d456f737436343873514f5941654a53395137626655566f4d476e6a6f34415a64554d516b7535304d6344634d57634250767230537a625441464466764a71774c7a67787741546e43676e703477446c3641612b41783238336767686d6a2b766a37666545324b4242524d5733467a4f704c4f41446c3049736235353837682f55346747766b74357636305a31564c47384268596a627a527779515a656d77416436634352352f5846574c595a52494d70583339415230746a61474769477a4c5679687365354339524b433661693432707057504b694261674f7661596b386c4f3744616a657261624f5a5034364c627935774b6a77314843527837703973564d4f57477a622f7641316877695763366a6d334d765144546f67516b697149684a56306e42514254552b336f6b4b434644793957776665726b486a74786962377433784955517448786e49777478346d706732362f486677564e564462346f493952486d78355747656c52566c7274697734337a626f434c6178763436415a654233496c546b776f75656254723179324e6a5370487a3638574e466a487675707933713854466e33486f733249416b344a753564436f3842337750375650722f4647614b69472b542b762b54517149724f714d544c31566457563144646d63624f384b58427a3665736d5957594b5077444c35623546413161306877617048696f6d30722f634b616f71722b32372f58637253355577534d625141414141424a52553545726b4a6767673d3d" alt="DeepWiki"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/62311d24e520b1e4b51d3da006531713abc73b8341cbdaab1060efbbc80403c7/68747470733a2f2f706b672e676f2e6465762f62616467652f6769746875622e636f6d2f6f70656e6667612f6f70656e6667612e737667" alt="Go Reference"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/45cddeca948720749c64633633de288d8f4f4f21edaa98299c906339d3411333/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f6f70656e6667612f6f70656e6667613f736f72743d73656d76657226636f6c6f723d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/45cddeca948720749c64633633de288d8f4f4f21edaa98299c906339d3411333/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f6f70656e6667612f6f70656e6667613f736f72743d73656d76657226636f6c6f723d677265656e" alt="GitHub release (latest SemVer)"&gt;&lt;/a&gt;
&lt;a href="https://hub.docker.com/r/openfga/openfga/tags" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/20068d834aa28f358bab4a77235bcc490d403b85530c060f903a1d506e6f2b29/68747470733a2f2f696d672e736869656c64732e696f2f646f636b65722f70756c6c732f6f70656e6667612f6f70656e666761" alt="Docker Pulls"&gt;&lt;/a&gt;
&lt;a href="https://app.codecov.io/gh/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3b804d3b9b2fe497663ae7c6c3fcbaa113a96784cd187e79d8584013e662204f/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f6f70656e6667612f6f70656e666761" alt="Codecov"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d38b039069e9f851f6fa7a7054574ba79b0728d8bbbf93650f8200639a0b04f6/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f6f70656e6667612f6f70656e666761" alt="Go Report"&gt;&lt;/a&gt;
&lt;a href="https://bestpractices.coreinfrastructure.org/projects/6374" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/45419164499d5394f5a2c35e21c204c3333b312684c859549724fccf274e89f4/68747470733a2f2f626573747072616374696365732e636f7265696e6672617374727563747572652e6f72672f70726f6a656374732f363337342f6261646765" alt="CII Best Practices"&gt;&lt;/a&gt;
&lt;a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fopenfga%2Fopenfga?ref=badge_shield" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/bef4aa81afbc0cacc2aae993ff14f8c13b688533ec88cc6ffd9048df9275510e/68747470733a2f2f6170702e666f7373612e636f6d2f6170692f70726f6a656374732f6769742532426769746875622e636f6d2532466f70656e6667612532466f70656e6667612e7376673f747970653d736869656c64" alt="FOSSA Status"&gt;&lt;/a&gt;
&lt;a href="https://artifacthub.io/packages/helm/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/60e76d2b195ead6429362291c1e810ff37f992e2fd592c647f64a95171061665/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f61727469666163746875622e696f2f62616467652f7265706f7369746f72792f6f70656e666761" alt="Artifact HUB"&gt;&lt;/a&gt;
&lt;a href="https://securityscorecards.dev/viewer/?uri=github.com/openfga/openfga" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/bc4da413504ba775667612ad1f09eda9581eda6fab9551ee8ca19e5621408f92/68747470733a2f2f6170692e736563757269747973636f726563617264732e6465762f70726f6a656374732f6769746875622e636f6d2f6f70656e6667612f6f70656e6667612f6261646765" alt="OpenSSF Scorecard"&gt;&lt;/a&gt;
&lt;a href="https://slsa.dev" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dc294f15fb5f1c96307863a1e96860310be940504e7ee370cee94bf4400cbac9/68747470733a2f2f736c73612e6465762f696d616765732f67682d62616467652d6c6576656c332e737667" alt="SLSA 3"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;OpenFGA&lt;/strong&gt; is a high-performance, flexible authorization/permission engine inspired by &lt;a href="https://research.google/pubs/pub48190/" rel="nofollow noopener noreferrer"&gt;Google Zanzibar&lt;/a&gt;
It helps developers easily model and enforce fine-grained access control in their applications.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Highlights&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;⚡ High-performance, developer-friendly APIs (HTTP &amp;amp; gRPC)&lt;/li&gt;
&lt;li&gt;🔌 Flexible storage backends (In-Memory, PostgreSQL, MySQL, SQLite beta)&lt;/li&gt;
&lt;li&gt;🧰 SDKs for &lt;a href="https://central.sonatype.com/artifact/dev.openfga/openfga-sdk" rel="nofollow noopener noreferrer"&gt;Java&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@openfga/sdk" rel="nofollow noopener noreferrer"&gt;Node.js&lt;/a&gt;, &lt;a href="https://github.com/openfga/go-sdk" rel="noopener noreferrer"&gt;Go&lt;/a&gt;, &lt;a href="https://github.com/openfga/python-sdk" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, &lt;a href="https://www.nuget.org/packages/OpenFga.Sdk" rel="nofollow noopener noreferrer"&gt;.NET&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐  Several additional SDKs and tools &lt;a href="https://github.com/openfga/community#community-projects" rel="noopener noreferrer"&gt;contributed by the community&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧪 &lt;a href="https://github.com/openfga/cli" rel="noopener noreferrer"&gt;CLI&lt;/a&gt; for interacting with an OpenFGA server and &lt;a href="https://openfga.dev/docs/modeling/testing" rel="nofollow noopener noreferrer"&gt;testing authorization models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌿 &lt;a href="https://github.com/openfga/terraform-provider-openfga" rel="noopener noreferrer"&gt;Terraform Provider&lt;/a&gt; for configuring OpenFGA servers as code&lt;/li&gt;
&lt;li&gt;🎮 &lt;a href="https://openfga.dev/docs/getting-started/setup-openfga/playground" rel="nofollow noopener noreferrer"&gt;Playground&lt;/a&gt; for modeling and testing&lt;/li&gt;
&lt;li&gt;🛠 Can also be embedded as a &lt;a href="https://pkg.go.dev/github.com/openfga/openfga/pkg/server#example-NewServerWithOpts" rel="nofollow noopener noreferrer"&gt;Go library&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤝 Adopted by &lt;a href="https://fga.dev" rel="nofollow noopener noreferrer"&gt;Auth0&lt;/a&gt;, &lt;a href="https://grafana.com/" rel="nofollow noopener noreferrer"&gt;Grafana Labs&lt;/a&gt;, &lt;a href="https://canonical.com/" rel="nofollow noopener noreferrer"&gt;Canonical&lt;/a&gt;, &lt;a href="https://docker.com" rel="nofollow noopener noreferrer"&gt;Docker&lt;/a&gt;,  &lt;a href="https://agicap.com" rel="nofollow noopener noreferrer"&gt;Agicap&lt;/a&gt;, &lt;a href="https://read.ai" rel="nofollow noopener noreferrer"&gt;Read.AI&lt;/a&gt; and &lt;a href="https://github.com/openfga/community/blob/main/ADOPTERS.md" rel="noopener noreferrer"&gt;others&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of Contents&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/openfga/openfga#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#docker" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#docker-compose" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#homebrew" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#precompiled-binaries" rel="noopener noreferrer"&gt;Precompiled Binaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#build-from-source" rel="noopener noreferrer"&gt;Build from Source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#verify-installation" rel="noopener noreferrer"&gt;Verify Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#playground" rel="noopener noreferrer"&gt;Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#next-steps" rel="noopener noreferrer"&gt;Next Steps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#limitations" rel="noopener noreferrer"&gt;Limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#production-readiness" rel="noopener noreferrer"&gt;Production Readiness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga#contributing--community" rel="noopener noreferrer"&gt;Contributing &amp;amp; Community&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-alert markdown-alert-important"&gt;
&lt;p class="markdown-alert-title"&gt;Important&lt;/p&gt;
&lt;p&gt;The following steps are meant…&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/openfga/openfga" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Your future self will thank you for choosing relationships over nested IF statements.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1253749"&gt;
    &lt;a href="/getlarge" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="getlarge image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/getlarge"&gt;Edouard Maleix&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/getlarge"&gt;I am a Senior Software Engineer, focusing on distributed systems, application security and developer productivity/creativity.
Based in Vienna 🥐, Austria.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openfga.dev/docs" rel="noopener noreferrer"&gt;OpenFGA Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openfga/openfga" rel="noopener noreferrer"&gt;OpenFGA GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zanzibar.academy" rel="noopener noreferrer"&gt;Zanzibar Academy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://storage.googleapis.com/gweb-research2023-media/pubtools/5068.pdf" rel="noopener noreferrer"&gt;Google's Zanzibar Paper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ory.sh/keto/docs/" rel="noopener noreferrer"&gt;Ory Keto Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openfga.dev/docs/best-practices/running-in-production" rel="noopener noreferrer"&gt;OpenFGA production best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tutorial</category>
      <category>openfga</category>
      <category>authorization</category>
      <category>security</category>
    </item>
    <item>
      <title>Building Single Executable Applications with Node.js</title>
      <dc:creator>Edouard Maleix</dc:creator>
      <pubDate>Mon, 17 Mar 2025 10:50:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/building-single-executable-applications-with-nodejs-16k3</link>
      <guid>https://dev.to/playfulprogramming/building-single-executable-applications-with-nodejs-16k3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Can we upgrade to the latest Node.js version?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few months ago, I did not imagine that this seemingly innocent question from a client would bring me so &lt;strong&gt;deep&lt;/strong&gt; into the world of single executable applications, a.k.a. SEA, a.k.a. executable binaries.&lt;/p&gt;

&lt;p&gt;Their team had been using &lt;a href="https://github.com/vercel/pkg" rel="noopener noreferrer"&gt;PKG&lt;/a&gt; to package Node.js applications into standalone executables, but then they hit a roadblock - PKG has been deprecated. While the community &lt;a href="https://github.com/yao-pkg/pkg" rel="noopener noreferrer"&gt;fork&lt;/a&gt; had kept pace with Node.js evolution (even working on Node.js v22), the constant changes to Node.js internals made maintaining these patches challenging. Each new &lt;strong&gt;Node.js version required adapting to internal changes&lt;/strong&gt;, creating an ongoing maintenance effort that added complexity to their deployment pipeline.&lt;/p&gt;

&lt;p&gt;That's when I remembered hearing about Node.js &lt;strong&gt;Single Executable Applications (SEA)&lt;/strong&gt; - a native feature. As someone who's spent years optimizing deployment pipelines and container strategies, I knew I had to explore this promising alternative.&lt;/p&gt;

&lt;p&gt;In this article, I'm sharing what I've learned about SEA, a feature that fundamentally shifts how we package and distribute Node.js applications. If you've encountered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delays waiting for packaging tools to support new Node.js versions&lt;/li&gt;
&lt;li&gt;Complex configuration requirements for native modules&lt;/li&gt;
&lt;li&gt;The maintenance burden of keeping dependencies versions and build tools in sync with runtime&lt;/li&gt;
&lt;li&gt;The search for lighter, more portable deployment artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...then you'll find this exploration valuable.&lt;/p&gt;

&lt;p&gt;I'll take you through both the technical foundations and practical implementation of SEA, showing how it can simplify your deployment strategy while making your applications more portable and secure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Two Worlds of Application Distribution&lt;/li&gt;
&lt;li&gt;Technical Foundation&lt;/li&gt;
&lt;li&gt;Practical Implementation&lt;/li&gt;
&lt;li&gt;Docker Integration&lt;/li&gt;
&lt;li&gt;Performance Benchmarks&lt;/li&gt;
&lt;li&gt;The Reality of Code Protection&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Two Worlds of Application Distribution
&lt;/h2&gt;

&lt;p&gt;First, I want to distinguish between each distribution method.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional Node.js Deployment
&lt;/h3&gt;

&lt;p&gt;The conventional approach to Node.js deployment has several limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fragmented Distribution&lt;/strong&gt;: Source files, dependencies, and runtime are all separate pieces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Dependencies&lt;/strong&gt;: Applications only work when the target environment has the exact right setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration Fragility&lt;/strong&gt;: Settings files must be correctly placed and formatted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Constraints&lt;/strong&gt;: The correct Node.js version must be present&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a brittle deployment pipeline where one small misconfiguration can cause everything to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Executable Applications Deployment
&lt;/h3&gt;

&lt;p&gt;Single Executable Applications (SEA) represent a fundamental paradigm shift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Self-Containment&lt;/strong&gt;: Code, dependencies, assets, and runtime bundled in one binary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Independence&lt;/strong&gt;: The application carries its environment with it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protected Configuration&lt;/strong&gt;: Settings embedded and secured within the executable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Simplicity&lt;/strong&gt;: One file to deploy, one file to run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach represents a &lt;strong&gt;shift-left&lt;/strong&gt; in application delivery—moving deployment concerns from operations teams into the development process. With SEA, developers take ownership of packaging the entire application environment during development rather than leaving it to deployment time.&lt;/p&gt;

&lt;p&gt;The result? Applications that are more reliable, more secure, and significantly easier to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Foundation
&lt;/h2&gt;

&lt;p&gt;As a longtime Node.js developer, I was curious about how this feature was implemented. Let's peek under the hood!&lt;/p&gt;

&lt;h3&gt;
  
  
  The Anatomy of a SEA
&lt;/h3&gt;

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

&lt;p&gt;A Single Executable Application builds in three distinct stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Collection Stage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual File System Stage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary Integration Stage&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Collection Stage
&lt;/h4&gt;

&lt;p&gt;This is where things start getting interesting! The SEA tooling gathers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript application code&lt;/li&gt;
&lt;li&gt;JSON configuration files&lt;/li&gt;
&lt;li&gt;Dependencies from &lt;code&gt;node_modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native add-ons&lt;/li&gt;
&lt;li&gt;Static assets&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Virtual File System Stage
&lt;/h4&gt;

&lt;p&gt;This is where the real magic happens. Instead of just bundling files, SEA creates a miniature in-memory filesystem that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserves directory structures exactly as they were&lt;/li&gt;
&lt;li&gt;Maintains file permissions&lt;/li&gt;
&lt;li&gt;Keeps symbolic links intact&lt;/li&gt;
&lt;li&gt;Retains original file paths&lt;/li&gt;
&lt;li&gt;Enables fast random access&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Binary Integration Stage
&lt;/h4&gt;

&lt;p&gt;The final step injects our virtual filesystem into the Node.js binary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserves execution ability&lt;/li&gt;
&lt;li&gt;Maintains code signing compatibility&lt;/li&gt;
&lt;li&gt;Enables runtime detection&lt;/li&gt;
&lt;li&gt;Supports cross-platform formats (Mach-O, PE, ELF)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Internals
&lt;/h2&gt;

&lt;p&gt;The conductor behind the SEA orchestra is a small piece of C++ code in the Node.js source.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; This is the nerdy part, feel free to jump to the Practical Implementation if you're more interested in hands-on examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SEA Configuration
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/nodejs/node/blob/main/src/node_sea.cc" rel="noopener noreferrer"&gt;&lt;code&gt;node_sea.cc&lt;/code&gt;&lt;/a&gt; evolves around the &lt;code&gt;SeaResource&lt;/code&gt; structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SeaResource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nl"&gt;public:&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;kMagic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x1EA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// SEA magic number&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;kHeaderSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Size of header&lt;/span&gt;

  &lt;span class="n"&gt;SeaFlags&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt; &lt;span class="n"&gt;code_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt; &lt;span class="n"&gt;main_code_or_snapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;code_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;unordered_map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This structure shows us several critical design decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use of a magic number (0x1EA) to identify the injected program during deserialization&lt;/li&gt;
&lt;li&gt;Support for both code and snapshots&lt;/li&gt;
&lt;li&gt;Optional location for code cache (we'll talk about it later)&lt;/li&gt;
&lt;li&gt;Asset storage in a key-value format&lt;/li&gt;
&lt;/ol&gt;


&lt;h3&gt;
  
  
  SEA Generation Process
&lt;/h3&gt;

&lt;p&gt;The high-level process of the SEA creation is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the main (bundled) script&lt;/li&gt;
&lt;li&gt;Generate V8 snapshot if needed (I will talk about snapshot later)&lt;/li&gt;
&lt;li&gt;Generate code cache if requested (I will talk about cache later)&lt;/li&gt;
&lt;li&gt;Processes and includes assets&lt;/li&gt;
&lt;li&gt;Serialize everything into a single Blob
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;ExitCode&lt;/span&gt; &lt;span class="nf"&gt;GenerateSingleExecutableBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SeaConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;exec_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;main_script&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;ReadFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;main_script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c_str&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;SeaFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;kUseSnapshot&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GenerateSnapshotForSEA&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;SeaFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;kUseCodeCache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GenerateCodeCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;main_script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;unordered_map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;BuildAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;SeaSerializer&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Resource Injection
&lt;/h3&gt;

&lt;p&gt;SEA uses the postject library to add our VFS as a new section in the binary file format.&lt;/p&gt;

&lt;p&gt;Different operating systems use different binary formats. Node.js support the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS uses Mach-O&lt;/li&gt;
&lt;li&gt;Windows uses Portable Executable (PE)&lt;/li&gt;
&lt;li&gt;Linux uses ELF (Executable and Linkable Format)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt; &lt;span class="nf"&gt;FindSingleExecutableBlob&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#ifdef __APPLE__
&lt;/span&gt;  &lt;span class="n"&gt;postject_options&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;postject_options_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;macho_segment_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"NODE_SEA"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;postject_find_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NODE_SEA_BLOB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;postject_find_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NODE_SEA_BLOB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Read-Only Assets
&lt;/h3&gt;

&lt;p&gt;One important security feature is read-only asset access.&lt;br&gt;
The implementation ensures assets remain read-only through careful API design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assets are stored as immutable string_views&lt;/li&gt;
&lt;li&gt;The ArrayBuffer is created with a no-op deleter&lt;/li&gt;
&lt;li&gt;No API exists for modifying assets once bundled
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GetAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FunctionCallbackInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Validate input&lt;/span&gt;
  &lt;span class="n"&gt;CHECK_EQ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="n"&gt;Utf8Value&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetIsolate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="n"&gt;SeaResource&lt;/span&gt; &lt;span class="n"&gt;sea_resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FindSingleExecutableResource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sea_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sea_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;unique_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;v8&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BackingStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NewBackingStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;const_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;[](&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;  &lt;span class="c1"&gt;// No-op deleter prevents modifications&lt;/span&gt;
      &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;Local&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetIsolate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetReturnValue&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Load the SEA
&lt;/h3&gt;

&lt;p&gt;The final step is loading the SEA and executing the bundled code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gets the current V8 context and environment&lt;/li&gt;
&lt;li&gt;Finds the SEA resource using &lt;code&gt;FindSingleExecutableResource()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verifies it's not using a snapshot (&lt;code&gt;CHECK(!sea.use_snapshot())&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Converts the main code into a V8 value using &lt;code&gt;ToV8Value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Calls the CJS (CommonJS) run callback with the converted code (from this, you can guess that only CJS is supported!)
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;MaybeLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LoadSingleExecutableApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;StartExecutionCallbackInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Local&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Isolate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetCurrent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetCurrentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetCurrent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;SeaResource&lt;/span&gt; &lt;span class="n"&gt;sea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FindSingleExecutableResource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;sea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_snapshot&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;Local&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;main_script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;ToV8Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main_code_or_snapshot&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ToLocalChecked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_cjs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isolate&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;main_script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;I built a file management service using Fastify to demonstrate SEA capabilities in the real world. The complete source code is available in the &lt;a href="https://github.com/getlarge/node-sea-demo" rel="noopener noreferrer"&gt;Node-SEA Demo repository&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hopefully, &lt;a href="https://github.com/lirantal" rel="noopener noreferrer"&gt;Liran Tal&lt;/a&gt; won't put the shame on me with this example app, no input validation, no rate limit, no auth, at least there's a path traversal check ;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;

&lt;p&gt;The project structure is simple, while representing a typical Node.js HTTP API application:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;sea&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;demo&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;                 &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Entry&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;SEA&lt;/span&gt; &lt;span class="nx"&gt;detection&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;             &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;         &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;                &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Static&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;sea&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;SEA&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;              &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Build&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Implementation Details
&lt;/h3&gt;

&lt;p&gt;The first challenge I encountered was detecting whether we're running in a SEA environment to override the &lt;code&gt;require&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sea&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:sea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSea&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRequire&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;require&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRequire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;trustProxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;When running as a SEA, &lt;code&gt;require.cache&lt;/code&gt; is undefined! The bug is tracked &lt;a href="https://github.com/nodejs/node/issues/49163" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another pain point was managing Fastify plugins within the SEA environment. I found explicit registration works better than auto-loading, to avoid compilation issues:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyInstance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sensible&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fastifyMultipart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 10MB&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAssetsDir&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fastifyStatic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assetsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/assets/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Security was another concern. Since SEA applications might also run with elevated privileges, I implemented strict path safety:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safePathResolve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userPath&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[/\\]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolvedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resolvedPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseDir&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPath&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Path traversal attempt detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resolvedPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPath&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Path resolution error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Build Process
&lt;/h3&gt;

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

&lt;p&gt;The first step in creating an SEA is properly bundling your JavaScript. I explored several bundlers and found ESBuild offers the best balance of speed and ease of use.&lt;/p&gt;
&lt;h4&gt;
  
  
  Bundling with ESBuild
&lt;/h4&gt;

&lt;p&gt;ESBuild prepares your application for SEA packaging through CLI or build tools.&lt;/p&gt;
&lt;h4&gt;
  
  
  Direct ESBuild Command
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx esbuild &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cjs &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bundle&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tsconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apps/node-sea-demo/tsconfig.app.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist/apps/node-sea-demo &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--out-extension&lt;/span&gt;:.js&lt;span class="o"&gt;=&lt;/span&gt;.js &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--outfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main.js &lt;span class="se"&gt;\&lt;/span&gt;
    apps/node-sea-demo/src/main.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each flag serves a specific purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--format=cjs&lt;/code&gt;: SEA only works with CommonJS (no ESM yet, remember?)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--platform=node&lt;/code&gt;: Targets Node.js environment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--bundle&lt;/code&gt;: Rolls up all imports into a single file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--outfile&lt;/code&gt;: Specifies the output file for the bundled code&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Nx ESBuild plugin Configuration
&lt;/h4&gt;

&lt;p&gt;If you're using Nx (like I do), you can configure the build in your project's configuration file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;project.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@nx/esbuild:esbuild"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{options.outputPath}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"defaultConfiguration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/apps/node-sea-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cjs"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"thirdParty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/node-sea-demo/src/main.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tsConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/node-sea-demo/tsconfig.app.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apps/node-sea-demo/src/assets/**/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"generatePackageJson"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"esbuildOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sourcemap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outExtension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;".js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".js"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Generating the Executable
&lt;/h3&gt;

&lt;p&gt;After struggling with single executables builder tools like PKG, I was glad to discover Node.js's native SEA support. The configuration is refreshingly simple:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sea-config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/apps/node-sea-demo/main.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/apps/node-sea-demo/demo.blob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disableExperimentalSEAWarning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"useCodeCache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"package.json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/apps/node-sea-demo/package.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The executable generation process varies by platform, if you are using Nx, you can make your life easier by using my &lt;a href="https://github.com/getlarge/nx-node-sea" rel="noopener noreferrer"&gt;plugin&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nx.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@getlarge/nx-node-sea"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apps/**/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"seaTargetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sea-build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"buildTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;.. and then run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nx run node-sea-demo:sea-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Otherwise, you can use the following commands for each platform, see the &lt;a href="https://nodejs.org/api/single-executable-applications.html#single-executable-applications" rel="noopener noreferrer"&gt;Node.js SEA documentation&lt;/a&gt; for more details:&lt;/p&gt;
&lt;h4&gt;
  
  
  Linux
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate blob&lt;/span&gt;
node &lt;span class="nt"&gt;--experimental-sea-config&lt;/span&gt; sea-config.json

&lt;span class="c"&gt;# Copy node binary&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node&lt;span class="si"&gt;)&lt;/span&gt; dist/app/node

&lt;span class="c"&gt;# Inject blob&lt;/span&gt;
npx postject dist/app/node NODE_SEA_BLOB dist/app/demo.blob &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sentinel-fuse&lt;/span&gt; NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  macOS
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate blob&lt;/span&gt;
node &lt;span class="nt"&gt;--experimental-sea-config&lt;/span&gt; sea-config.json

&lt;span class="c"&gt;# Copy node binary&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node&lt;span class="si"&gt;)&lt;/span&gt; dist/app/node

&lt;span class="c"&gt;# Remove signature&lt;/span&gt;
codesign &lt;span class="nt"&gt;--remove-signature&lt;/span&gt; dist/app/node

&lt;span class="c"&gt;# Inject blob&lt;/span&gt;
npx postject dist/app/node NODE_SEA_BLOB dist/app/demo.blob &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sentinel-fuse&lt;/span&gt; NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--macho-segment-name&lt;/span&gt; NODE_SEA

&lt;span class="c"&gt;# Re-sign binary&lt;/span&gt;
codesign &lt;span class="nt"&gt;--sign&lt;/span&gt; - dist/app/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Windows
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate blob&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--experimental-sea-config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sea-config.json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Copy node binary&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ProgramFiles&lt;/span&gt;&lt;span class="nx"&gt;\nodejs\node.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\dist\app\node.exe&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Inject blob&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;postject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dist/app/node.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;NODE_SEA_BLOB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dist/app/demo.blob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;^&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;--sentinel-fuse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can't just build once and run everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code cache and snapshots are platform-specific&lt;/li&gt;
&lt;li&gt;Native modules (still) require platform-specific compilation&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Docker Integration
&lt;/h2&gt;

&lt;p&gt;As someone who's built countless Docker images for Node.js apps, I couldn't wait to see how SEA could optimize container size.&lt;/p&gt;

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

&lt;p&gt;I experimented with three different approaches:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Full Image (235MB)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Uses complete Node.js runtime&lt;/li&gt;
&lt;li&gt;Includes OS dependencies&lt;/li&gt;
&lt;li&gt;Simplest to build and debug&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Image (156MB)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Uses scratch as base&lt;/li&gt;
&lt;li&gt;Includes only essential OS dependencies&lt;/li&gt;
&lt;li&gt;Smallest possible footprint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That 156MB image size might not seem impressive compared to Go or Rust applications' Docker image, but it's a dramatic improvement over the typical 800MB+ Node.js image!&lt;/p&gt;
&lt;h3&gt;
  
  
  Full image
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Uses full Node.js runtime - Contains development dependencies (OS)&lt;/li&gt;
&lt;li&gt;Create dedicated user (always good)&lt;/li&gt;
&lt;li&gt;Copy ESBuild bundle and change permissions&lt;/li&gt;
&lt;li&gt;No need to install deps, dependencies are bundled&lt;/li&gt;
&lt;li&gt;Run the app with node
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; docker.io/node:lts-alpine&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; HOST=0.0.0.0&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; node-sea-demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; node-sea-demo node-sea-demo

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; dist/apps/node-sea-demo node-sea-demo/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; node-sea-demo:node-sea-demo .

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "node", "node-sea-demo" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Minimal image
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;alpine to start and dependencies to build&lt;/li&gt;
&lt;li&gt;Install glibc for proper library copying&lt;/li&gt;
&lt;li&gt;copy bundled code and SEA config&lt;/li&gt;
&lt;li&gt;Bundle and remove debugging information ( # Strip the binary to remove debug symbols)&lt;/li&gt;
&lt;li&gt;Uses scratch as a base image,&lt;/li&gt;
&lt;li&gt;Eliminates unnecessary OS files&lt;/li&gt;
&lt;li&gt;Include minimal OS dependencies (loader) and copy the Node.js SEA executable&lt;/li&gt;
&lt;li&gt;Run the standalone executable
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:lts-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; build-base python3

&lt;span class="k"&gt;RUN &lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r1/glibc-2.35-r1.apk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; glibc-2.35-r1.apk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nb"&gt;rm &lt;/span&gt;glibc-2.35-r1.apk

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; dist/apps/node-sea-demo dist/apps/node-sea-demo&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/node-sea-demo/sea-config.json .&lt;/span&gt;

&lt;span class="c"&gt;# Create the SEA bundle and verify the file exists and is executable&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;node &lt;span class="nt"&gt;--experimental-sea-config&lt;/span&gt; sea-config.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node&lt;span class="si"&gt;)&lt;/span&gt; dist/apps/node-sea-demo/node &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  npx postject dist/apps/node-sea-demo/node NODE_SEA_BLOB dist/apps/node-sea-demo/node-sea-demo.blob &lt;span class="nt"&gt;--sentinel-fuse&lt;/span&gt; NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nb"&gt;chmod&lt;/span&gt; +x dist/apps/node-sea-demo/node &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="c"&gt;# # Strip the binary to remove debug symbols&lt;/span&gt;
  strip dist/apps/node-sea-demo/node

&lt;span class="c"&gt;# Create directory structure for dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; deps &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="c"&gt;# Copy all required shared libraries&lt;/span&gt;
  ldd dist/apps/node-sea-demo/node | grep "=&amp;gt; /" | awk '{print $3}' | \
  xargs -I '{}' cp -L '{}' deps/ &amp;amp;&amp;amp; \
  # Copy additional required files
  cp /lib/ld-linux-*.so.* deps/ &amp;amp;&amp;amp; \
  # Create necessary symlinks
  mkdir -p deps/lib64 &amp;amp;&amp;amp; \
  cp /lib/ld-linux-*.so.* deps/lib64/


&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/deps /lib/&lt;/span&gt;
&lt;span class="c"&gt;# Ensure lib64 exists and has the loader&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/deps/lib64 /lib64/&lt;/span&gt;
&lt;span class="c"&gt;# Copy the Node.js SEA executable&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist/apps/node-sea-demo/node /app/node&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; HOST=0.0.0.0&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; ${PORT}&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; [ "/app/node" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance Benchmarks
&lt;/h2&gt;

&lt;p&gt;Using the &lt;code&gt;useCodeCache&lt;/code&gt; option offers a dual benefit of better performance and slightly improved code protection.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sea-config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"useCodeCache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;caching&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I ran some benchmarks to see how the application performs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;regular Node.js (no SEA)&lt;/li&gt;
&lt;li&gt;running with code cache enabled&lt;/li&gt;
&lt;li&gt;running without code cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what I found:&lt;/p&gt;

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

&lt;p&gt;The results surprised me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code cache improved startup time by ~7%&lt;/li&gt;
&lt;li&gt;Blob size increased by only ~7% with code cache enabled&lt;/li&gt;
&lt;li&gt;Cold starts showed significant variance (typical for any Node.js app)&lt;/li&gt;
&lt;li&gt;Hot starts were consistently faster with code cache&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  How Code Cache Works
&lt;/h3&gt;

&lt;p&gt;When JavaScript code is executed, V8 (Node.js's JavaScript engine) compiles it to bytecode before execution. This compilation takes time. Code cache stores this pre-compiled bytecode, allowing V8 to skip the compilation step on subsequent runs.&lt;/p&gt;

&lt;p&gt;The performance improvement is most noticeable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application startup time (as shown in our benchmarks)&lt;/li&gt;
&lt;li&gt;Cold starts in serverless environments&lt;/li&gt;
&lt;li&gt;Complex codebase with many dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;For most applications, I'd definitely recommend enabling code cache despite the slight size increase.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The Reality of Code Protection
&lt;/h2&gt;

&lt;p&gt;One question that frequently comes up when discussing SEA is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Are our source code and proprietary algorithms safe inside the executable?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;While SEA bundles your code inside the Node.js binary, it's important to understand that &lt;strong&gt;this is not true obfuscation&lt;/strong&gt;. Let me demonstrate why.&lt;/p&gt;
&lt;h3&gt;
  
  
  Extracting Code from SEA Executables
&lt;/h3&gt;

&lt;p&gt;As said already, a SEA is a Node.js binary with your code injected as a resource section. With the right tools, extracting this section is relatively straightforward.&lt;/p&gt;

&lt;p&gt;On macOS, extracting code from a SEA executable is a bit more involved than on Linux due to the Mach-O binary format. Here's a demo:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. First, examine the Mach-O headers to locate the NODE_SEA segment&lt;/span&gt;
otool &lt;span class="nt"&gt;-l&lt;/span&gt; dist/apps/node-sea-demo/node | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 20 NODE_SEA

&lt;span class="c"&gt;# 2. Note the offset, size, and address of the NODE_SEA segment&lt;/span&gt;
&lt;span class="c"&gt;# The output will show something like:&lt;/span&gt;
  segname NODE_SEA
   vmaddr 0x0000000104e20000
   vmsize 0x0000000000254000
  fileoff 81739776
 filesize 2432242
  maxprot 0x00000001
 initprot 0x00000001
   nsects 1
    flags 0x0
Section
  sectname __NODE_SEA_BLOB
   segname NODE_SEA
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# 3. Extract the segment using dd, extract from the offset &amp;lt;fileoff&amp;gt; and size &amp;lt;filesize&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-sea-app &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;extracted.blob &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;skip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;81739776 &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2432242
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Examining the Extracted Content
&lt;/h3&gt;

&lt;p&gt;Once extracted, the blob contains the bundled JavaScript code in a readable format. Let's see what it actually looks like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Look for readable strings&lt;/span&gt;
strings extracted.blob | less

&lt;span class="c"&gt;# For function declarations&lt;/span&gt;
strings extracted.blob | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"function"&lt;/span&gt; | less

&lt;span class="c"&gt;# Targeting assets&lt;/span&gt;
strings extracted.blob | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"assets"&lt;/span&gt; | less
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;blockquote&gt;
&lt;p&gt;Amazing, right?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you can see, the original source code remains largely intact and readable. This is why relying solely on SEA for code protection is insufficient for truly sensitive intellectual property.&lt;/p&gt;


&lt;h3&gt;
  
  
  How can we protect our code?
&lt;/h3&gt;

&lt;p&gt;For genuinely sensitive code, I recommend a multi-layered approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use SEA with Bytenode for basic protection (be aware it was already &lt;a href="https://swarm.ptsecurity.com/how-we-bypassed-bytenode-and-decompiled-node-js-bytecode-in-ghidra/" rel="noopener noreferrer"&gt;reversed engineered&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Keep truly sensitive algorithms on a secure server accessed via API&lt;/li&gt;
&lt;li&gt;Consider legal protections (contracts, terms of service)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: no code shipped to a client's machine can ever be 100% protected against a determined adversary with sufficient resources and time.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Node.js Single Executable Applications have transformed how I deliver Node.js applications, offering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved deployment reliability&lt;/li&gt;
&lt;li&gt;Better security through bundling&lt;/li&gt;
&lt;li&gt;Reduced environmental dependencies&lt;/li&gt;
&lt;li&gt;Optimized resource usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After years of dubious deployment strategy, I can now distribute my Node.js applications as single files that "just work" on the target system.&lt;/p&gt;

&lt;p&gt;Want to try it yourself? Here are some next steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Experiment with the provided example application&lt;/li&gt;
&lt;li&gt;Evaluate SEA for your specific use cases&lt;/li&gt;
&lt;li&gt;Implement in your CI/CD pipeline&lt;/li&gt;
&lt;li&gt;Join the Node.js SEA community and contribute!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have questions or want to share your SEA experiences, drop a comment below or reach out to me on &lt;a href="https://www.linkedin.com/in/edouard-maleix/" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt; or &lt;a href="https://github.com/getlarge" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1253749"&gt;
    &lt;a href="/getlarge" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="getlarge image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/getlarge"&gt;Edouard Maleix&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/getlarge"&gt;I am a Senior Software Engineer, focusing on distributed systems, application security and developer productivity/creativity.
Based in Vienna 🥐, Austria.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>node</category>
      <category>tutorial</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>Dynamic NestJS Listeners: Discover the Power of Lazy Loading</title>
      <dc:creator>Edouard Maleix</dc:creator>
      <pubDate>Sun, 13 Oct 2024 11:46:34 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/dynamic-nestjs-listeners-discover-the-power-of-lazy-loading-53i2</link>
      <guid>https://dev.to/playfulprogramming/dynamic-nestjs-listeners-discover-the-power-of-lazy-loading-53i2</guid>
      <description>&lt;p&gt;In this post, I will show you how to register HTTP routes and message consumers in NestJS applications &lt;strong&gt;dynamically&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This demonstration will start by uncovering some NestJS internals, particularly an undocumented feature: the &lt;code&gt;DiscoveryModule&lt;/code&gt; 🧐.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By dynamic routes, I mean routes unknown at the time of development but defined at runtime, late in the boot process.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If reading bores you, skip the talk and jump directly to the &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;The declarative approach is &lt;strong&gt;great&lt;/strong&gt; but can also bring some &lt;strong&gt;limitations&lt;/strong&gt;.&lt;br&gt;
In the routing case, the decorators used to declare HTTP routes and microservices listeners (&lt;code&gt;@Get&lt;/code&gt;, &lt;code&gt;@Post&lt;/code&gt;, &lt;code&gt;@EventPattern&lt;/code&gt;, &lt;code&gt;@MessagePattern&lt;/code&gt;) require &lt;strong&gt;static&lt;/strong&gt; values known during development time and cannot be &lt;strong&gt;late-bound&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Get('users')&lt;/code&gt; defines a static route that can only change by modifying the code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@MessagePattern('user.created')&lt;/code&gt; is not adaptable to dynamic topic structures.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Business Impact&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modular Plugin Architecture&lt;/td&gt;
&lt;td&gt;Allow developers to create modular plugins that can be easily integrated into a core application without modifying the underlying code.&lt;/td&gt;
&lt;td&gt;A web development platform allows users to install plugins that add new features, each with its own set of HTTP routes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White Labeling for E-commerce Platforms&lt;/td&gt;
&lt;td&gt;Enable e-commerce platforms to offer white-labeled application versions to different clients without modifying the underlying codebase.&lt;/td&gt;
&lt;td&gt;A popular e-commerce platform allows its clients (e.g., online retailers) to create custom platform versions with unique routes and listeners.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customizable Integration Layers&lt;/td&gt;
&lt;td&gt;Allow customers to customize the integration layer of an application based on their specific business needs without requiring code changes.&lt;/td&gt;
&lt;td&gt;A CRM system allows users to integrate it with various external services (e.g., email marketing platforms and sales tools).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature Flags for Enterprise Applications&lt;/td&gt;
&lt;td&gt;Enable enterprise applications to easily toggle certain features on or off based on user roles or organizational policies.&lt;/td&gt;
&lt;td&gt;A large enterprise application allows administrators to toggle specific features (e.g., access to advanced analytics tools) on or off for different departments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Tenancy in SaaS Applications&lt;/td&gt;
&lt;td&gt;Allow Software-as-a-Service (SaaS) applications to support multiple tenants with their custom routes and listeners.&lt;/td&gt;
&lt;td&gt;A SaaS-based customer engagement platform allows clients to create custom application versions with unique routes and listeners.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  NestJS scanning and registration lifecycle
&lt;/h2&gt;

&lt;p&gt;Word to the wise: to declare dynamic routes and listeners, we must first understand how NestJS inspects modules and scans the metadata to register them.&lt;/p&gt;

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

&lt;p&gt;For both HTTP routes and microservices listeners, NestJS will scan the metadata and register the routes and listeners during the initialization phase when &lt;code&gt;NestApplication.init&lt;/code&gt; and &lt;code&gt;NestMicroservice.init&lt;/code&gt; are called.&lt;/p&gt;

&lt;p&gt;However, each of them has its own registration process and components:&lt;/p&gt;
&lt;h3&gt;
  
  
  HTTP routes
&lt;/h3&gt;

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

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

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

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When calling &lt;code&gt;NestApplication.listen&lt;/code&gt; and &lt;code&gt;NestMicroservice.listen&lt;/code&gt;, the &lt;code&gt;init&lt;/code&gt; methods will be called if they were not before.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have the following options to register dynamic routes and listeners:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create custom explorers as providers that &lt;strong&gt;scan the metadata&lt;/strong&gt; and replace the placeholders with actual values&lt;/li&gt;
&lt;li&gt;create custom adapters for HTTP API by implementing the &lt;a href="https://github.com/nestjs/nest/blob/master/packages/common/interfaces/http/http-server.interface.ts" rel="noopener noreferrer"&gt;&lt;code&gt;HttpServer&lt;/code&gt;&lt;/a&gt; interface&lt;/li&gt;
&lt;li&gt;create custom transport strategies for microservices by extending &lt;a href="https://github.com/nestjs/nest/blob/master/packages/microservices/server/server.ts" rel="noopener noreferrer"&gt;&lt;code&gt;Server&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 1&lt;/strong&gt; is the most straightforward and will be the focus of this article.&lt;/p&gt;
&lt;h2&gt;
  
  
  Create a Proof of Concept
&lt;/h2&gt;

&lt;p&gt;The demonstration is a simple NestJS application with a &lt;strong&gt;single HTTP route&lt;/strong&gt;, a &lt;strong&gt;single message consumer&lt;/strong&gt;, and the following requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable lazy evaluation of routes or message patterns with custom decorators and metadata explorers&lt;/li&gt;
&lt;li&gt;Support for both HTTP routes and message consumers&lt;/li&gt;
&lt;li&gt;Use environment variables to define the route and message pattern prefixes&lt;/li&gt;
&lt;li&gt;Replace the placeholders pattern before registering the routes and listeners (before &lt;a href="https://github.com/nestjs/nest/blob/v10.4.4/packages/core/nest-application.ts#L179" rel="noopener noreferrer"&gt;&lt;code&gt;NestJSApplication.init&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/nestjs/nest/blob/v10.4.4/packages/core/nest-application.ts#L221C10-L221C29" rel="noopener noreferrer"&gt;&lt;code&gt;app.connectMicroservice&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Seems like a good plan, but how can we set up the placeholders and replace them with actual values?&lt;/p&gt;

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

&lt;p&gt;We could use the &lt;a href="https://github.com/rbuckton/reflect-metadata" rel="noopener noreferrer"&gt;reflect-metadata&lt;/a&gt; library to store the metadata and then scan it &lt;strong&gt;manually&lt;/strong&gt; to replace the placeholders with actual values, but there is a better way.&lt;/p&gt;

&lt;p&gt;During my NestJS codebase review, I started following the &lt;code&gt;MetadataScanner&lt;/code&gt; thread and noticed it was also used by the &lt;code&gt;DiscoveryModule&lt;/code&gt;. This module provides a powerful tool for &lt;strong&gt;exploring and inspecting the providers, controllers, and modules&lt;/strong&gt; in your NestJS application, and it turns out to be the perfect tool for our use case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Funny enough, it has been in the NestJS core for several years but is not used internally. One could think it is part of the public API, but surprisingly, it is still undocumented. Do you also get this treasure hunt feeling? 🏴‍☠️&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since the approach is very similar for HTTP and microservices listeners, I only explain the registration process of HTTP routes. You can use the same approach to register the microservice listeners, as can be seen in this &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners/tree/main/libs/custom-message-pattern" rel="noopener noreferrer"&gt;internal library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The approch is based on the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a custom decorator&lt;/li&gt;
&lt;li&gt;Create the metadata scanner&lt;/li&gt;
&lt;li&gt;Declare the dynamic route&lt;/li&gt;
&lt;li&gt;Import the custom explorers&lt;/li&gt;
&lt;li&gt;Update ConfigService type and validator (optional)&lt;/li&gt;
&lt;li&gt;Testing (optional, or not 😏)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  1. Create a custom decorator
&lt;/h3&gt;

&lt;p&gt;For the HTTP routes, the starting point is the custom decorator: &lt;code&gt;CustomHttpMethod&lt;/code&gt;, which takes a &lt;em&gt;method&lt;/em&gt; and a &lt;em&gt;path&lt;/em&gt; as arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;path&lt;/em&gt; is a template string (e.g. ,&lt;code&gt;$HTTP_ROUTE_PREFIX/:id&lt;/code&gt;) containing placeholders that actual values will replace.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;method&lt;/em&gt; is one of the HTTP methods (e.g., &lt;strong&gt;GET&lt;/strong&gt;, &lt;strong&gt;POST&lt;/strong&gt;, etc).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This decorator is used above the controller methods to define the dynamic routes (e.g., &lt;code&gt;@CustomHttpMethod({ method: 'GET', path: '$HTTP_ROUTE_PREFIX/:id' })&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DiscoveryService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Method&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DiscoveryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createDecorator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;You can also find an example using only the Reflect API from &lt;code&gt;reflect-metadata&lt;/code&gt; library, in the &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners/blob/main/libs/custom-event-pattern/src/lib/custom-event-pattern.decorator.ts" rel="noopener noreferrer"&gt;example repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2. Create the metadata scanner
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Register the custom explorer
&lt;/h4&gt;

&lt;p&gt;The scanner is exposed as a dynamic NestJS module—&lt;code&gt;CustomHttpMethodModule&lt;/code&gt;—that can be imported into any module to enable dynamic HTTP routes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Dynamic module enhances reusability and allows for configuration from the outside.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The module is configurable with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;store&lt;/code&gt;: a map of keys (placeholders) and values (replacements).&lt;/li&gt;
&lt;li&gt;a list of &lt;code&gt;modules&lt;/code&gt; containing the controllers to scan for the custom decorators.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ICustomHttpMethodModuleOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;The module can be created using the &lt;code&gt;forRoot&lt;/code&gt; and &lt;code&gt;forRootAsync&lt;/code&gt; methods, which return a &lt;code&gt;DynamicModule&lt;/code&gt; object containing the module configuration.&lt;/li&gt;
&lt;li&gt;The module will import the &lt;code&gt;DiscoveryModule&lt;/code&gt; to enable the scanning of the controllers and methods.&lt;/li&gt;
&lt;li&gt;It provides and exports the &lt;code&gt;CustomHttpMethodExplorer&lt;/code&gt; service, which injects the &lt;code&gt;DiscoveryService&lt;/code&gt; and &lt;code&gt;MetadataScanner&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomHttpMethodModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ICustomHttpMethodModuleOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;DynamicModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DiscoveryModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModuleOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;CustomHttpMethodExplorer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CustomHttpMethodExplorer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;forRootAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModuleAsyncOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;DynamicModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imports&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DiscoveryModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DiscoveryModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAsyncProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;CustomHttpMethodExplorer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CustomHttpMethodExplorer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;createAsyncProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModuleAsyncOptions&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModuleOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inject&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid CustomHttpMethodModuleAsyncOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Iterate over the controllers and methods
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;CustomHttpMethodExplorer&lt;/code&gt; is a provider that will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;retrieve the controllers attached to the &lt;code&gt;modules&lt;/code&gt; provided in the options&lt;/li&gt;
&lt;li&gt;scan and iterate over each controller's methods&lt;/li&gt;
&lt;li&gt;search for methods that use the &lt;code&gt;CustomHttpMethod&lt;/code&gt; decorator&lt;/li&gt;
&lt;li&gt;substitute the placeholders with actual values from the &lt;code&gt;store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;decorate the method with the NestJS HTTP method decorator (e.g., &lt;code&gt;@Get&lt;/code&gt;, &lt;code&gt;@Post&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the benefits of using the &lt;a href="https://github.com/nestjs/nest/blob/master/packages/core/discovery/discovery-service.ts" rel="noopener noreferrer"&gt;&lt;code&gt;DiscoveryService&lt;/code&gt;&lt;/a&gt; is that it provides methods to explore the metadata of the controllers and methods [&lt;code&gt;getProviders&lt;/code&gt;, &lt;code&gt;getControllers&lt;/code&gt;, &lt;code&gt;getAllMethodNames&lt;/code&gt; and &lt;code&gt;getMetadataByDecorator&lt;/code&gt;], without having to use low-level APIs like &lt;code&gt;Reflect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus&lt;/strong&gt;: the &lt;code&gt;discoveryService.getMetadataByDecorator&lt;/code&gt; infers the type of the metadata.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;code&gt;CustomHttpMethodExplorer&lt;/code&gt; service is instantiated when the module is imported and will scan the controllers and methods to replace the placeholders with actual values.&lt;/em&gt; &amp;gt; &lt;em&gt;Alternatively, you can manually trigger the exploration in the &lt;code&gt;main.ts&lt;/code&gt; file.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomHttpMethodExplorer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CustomHttpMethodExplorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;methodsMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;PUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;PATCH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;HEAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CustomHttpMethodModuleOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomHttpMethodModuleOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;discoveryService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DiscoveryService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;metadataScanner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MetadataScanner&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;substituteValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\$(\w&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;getMethodDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MethodDecorator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;methodMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discoveryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getControllers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadataScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllMethodNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discoveryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMetadataByDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;CustomHttpMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;handler&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fulfilledPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substituteValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decorator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMethodDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fulfilledPath&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;
          &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metatype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOwnPropertyDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;handler&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PropertyDescriptor&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Mapped {&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fulfilledPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;} route`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;If you like it more low level, you can also find an example using only Reflect API, in the &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners/blob/main/libs/custom-event-pattern/src/lib/custom-event-pattern.service.ts" rel="noopener noreferrer"&gt;example repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  3. Declare the dynamic route
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;CustomHttpMethod&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$HTTP_METHOD_PREFIX/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Import the custom explorers
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;app.module.ts&lt;/code&gt;, add the custom explorers to the imports array.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nx"&gt;CustomHttpMethodModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRootAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvironmentVariables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP_METHOD_PREFIX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP_METHOD_PREFIX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  5. Update ConfigService type and validator
&lt;/h3&gt;

&lt;p&gt;To provide a better developer experience, we can use the &lt;code&gt;class-transformer&lt;/code&gt; and &lt;code&gt;class-validator&lt;/code&gt; libraries to validate the configuration object and provide default values (any other validation library will do).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Expose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class-transformer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IsInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsPositive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Min&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class-validator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Expose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;HTTP_METHOD_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http-demo-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;ConfigModule&lt;/code&gt; is updated to use the &lt;code&gt;plainToInstance&lt;/code&gt; function from &lt;code&gt;class-transformer&lt;/code&gt; to transform the configuration object into an instance of the &lt;code&gt;EnvironmentVariables&lt;/code&gt; class.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;validateSync&lt;/code&gt; function from &lt;code&gt;class-validator&lt;/code&gt; validates the instance of the &lt;code&gt;EnvironmentVariables&lt;/code&gt; class.&lt;/li&gt;
&lt;li&gt;After this validation we can increase the &lt;code&gt;ConfigService&lt;/code&gt; type safety by including the &lt;code&gt;EnvironmentVariables&lt;/code&gt; class (e.g., &lt;code&gt;ConfigService&amp;lt;EnvironmentVariables, true&amp;gt;&lt;/code&gt;).
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validatedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plainToInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EnvironmentVariables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;excludeExtraneousValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;exposeDefaultValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validatedConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;skipMissingProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;validatedConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  6. Testing
&lt;/h3&gt;

&lt;p&gt;The workspace includes &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners/blob/main/apps/demo-1-e2e/src/demo-1/demo-1.spec.ts" rel="noopener noreferrer"&gt;E2E tests&lt;/a&gt; that will send:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP request to the dynamic route&lt;/li&gt;
&lt;li&gt;MQTT message to the dynamic listener&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to run the tests to see the demonstration in action.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
npx nx run demo-1:serve
&lt;span class="c"&gt;# in another terminal&lt;/span&gt;
nx run demo-1-e2e:e2e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;p&gt;With this knowledge, you can implement the following scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace the template string with a &lt;code&gt;printf&lt;/code&gt;-like syntax&lt;/li&gt;
&lt;li&gt;Fetch routes and listeners from a remote service or a database&lt;/li&gt;
&lt;li&gt;Build your explorer modules to create a plugin system&lt;/li&gt;
&lt;li&gt;Create a reusable generic controller to avoid repeating decorators&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sfeircode/nestjs-discovery-15kd"&gt;Create groups of controllers/providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/micalevisk/nestjs-tip-how-to-attach-decorators-to-all-controllers-without-at-once-bg7"&gt;Attach decorators to all controllers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed this demonstration and learned something new about NestJS internals.&lt;/p&gt;

&lt;p&gt;If you want to show your appreciation, you can find the code in this &lt;a href="https://github.com/getlarge/nestjs-dynamic-routes-and-listeners" rel="noopener noreferrer"&gt;repository&lt;/a&gt; and give it a star ⭐️.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1253749"&gt;
    &lt;a href="/getlarge" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1253749%2Fc4b87a1b-784c-491c-ab9e-1cd2a4df299c.jpeg" alt="getlarge image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/getlarge"&gt;Edouard Maleix&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/getlarge"&gt;I am a Senior Software Engineer, focusing on distributed systems, application security and developer productivity/creativity.
Based in Vienna 🥐, Austria.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>typescript</category>
      <category>nestjs</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
