{"id":4003,"date":"2026-05-15T14:50:31","date_gmt":"2026-05-15T11:50:31","guid":{"rendered":"https:\/\/upcloud.com\/global\/?p=4003"},"modified":"2026-06-02T12:42:08","modified_gmt":"2026-06-02T11:42:08","slug":"shoehorn-intelligent-developer-platform","status":"publish","type":"post","link":"https:\/\/upcloud.com\/global\/blog\/shoehorn-intelligent-developer-platform\/","title":{"rendered":"Shoehorn &#8211; Intelligent Developer Platform"},"content":{"rendered":"\n<p>We built Shoehorn out of frustration. Some internal developer portals act like frameworks: you install one, then spend a quarter sifting through unmaintained community plugins, writing TypeScript and React, just to make it presentable. Most people running these portals are platform engineers, not frontend developers. So the portal becomes a product they&#8217;re stuck maintaining, instead of a tool their developers can just use.<\/p>\n\n\n\n<p>Shoehorn starts from the opposite assumption: you should be able to use it, not develop it. Whether you reach the catalog from the CLI, Terraform, the web UI, or an MCP-connected IDE agent, it&#8217;s the same product end to end. You install it, it discovers your stuff, and it works.<\/p>\n\n\n\n<p>One view of your whole stack: every workload across every cluster, the cloud resources behind them, ownership, and scorecards. No flipping between cluster dashboards, cloud consoles, and team wikis to answer one question.<\/p>\n\n\n\n<p>The sovereignty story is downstream of that. Because Shoehorn is genuinely self-hostable in one Terraform apply, you can run it on infrastructure you control, in a jurisdiction you choose. That&#8217;s why it&#8217;s a natural fit for UpCloud customers. But sovereignty isn&#8217;t the pitch. Simplicity is. Sovereignty is what you get for free when the product is small enough that one person can run it.<\/p>\n\n\n\n<p>The rest of this post is what that looks like in practice.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What is Shoehorn<\/h2>\n\n\n\n<p>Shoehorn is an Intelligent Developer Platform: service catalog, scaffolding engine, scorecards, fuzzy search, multi-tenant by design. Same category as Backstage, Port, Cortex.<\/p>\n\n\n\n<p>The differentiator is operational. We built it for the 95% of teams that don&#8217;t have a dedicated platform team to maintain a portal. Opinionated defaults instead of a plugin ecosystem. Auto-discovery from Kubernetes, GitHub, and your IdP instead of hand-written manifests. One install, sensible defaults, working catalog inside an afternoon. One person can run it. That&#8217;s the whole product thesis.<\/p>\n\n\n\n<p>Architecturally, it&#8217;s a small set of Go microservices (<code>api<\/code>, <code>eventbus<\/code>, <code>worker<\/code>, <code>crawler<\/code>, <code>forge<\/code>) backed by Postgres 18, Meilisearch, Valkey, and Redpanda. Authentication is OIDC: Zitadel, Okta, or any compliant provider. Multi-tenant isolation is enforced at the database layer using Postgres row-level security, not application-layer filtering, so a forgotten <code>WHERE<\/code> clause can&#8217;t leak data across tenants.<\/p>\n\n\n\n<p>It ships as a Helm chart, and that&#8217;s enough if you want to drive it yourself. But for partners and customers who&#8217;d rather not assemble the pieces by hand, we ship something more direct.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/1-shoehorn-catalog-1024x541.png\" alt=\"-\" class=\"wp-image-80986\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">What it adds on top of a Kubernetes cluster<\/h2>\n\n\n\n<p>Platform engineers ask us this regularly: What does Shoehorn add over kubectl and ArgoCD?<\/p>\n\n\n\n<p>Shoehorn doesn&#8217;t replace either of them. It sits at a different layer.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>kubectl<\/code><\/strong> answers &#8220;what&#8217;s the state of <em>this resource<\/em>, in <em>this cluster<\/em>, <em>right now<\/em>?&#8221; It&#8217;s a per-cluster CRUD tool. Brilliant for operations. Useless for &#8220;which team owns the payment service?&#8221; or &#8220;which services are below tier-2 reliability?&#8221;, because those questions aren&#8217;t about resources, they&#8217;re about your <em>organisation<\/em>.<\/li>\n\n\n\n<li><strong>ArgoCD \/ Flux<\/strong> answer &#8220;is the cluster in sync with the git-declared desired state?&#8221; GitOps is the right way to deploy. But ArgoCD doesn&#8217;t know what a <em>service<\/em> is in business terms, what its dependencies are, who&#8217;s on call, or whether it has a runbook. It&#8217;s a synchronisation engine, not a catalog.<\/li>\n\n\n\n<li><strong>Shoehorn<\/strong> answers questions spanning clusters, repositories, teams, and time. Where does this service run? Who owns it? Which version is in prod? What&#8217;s its scorecard? Where&#8217;s the runbook? It&#8217;s a catalog plus a workflow layer plus a governance layer, built for <em>people<\/em> rather than resources.<\/li>\n<\/ul>\n\n\n\n<p>In day-to-day terms, here&#8217;s how the three pieces work together for a typical Shoehorn-on-Kubernetes setup:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/2-shoehorn-operations-1024x421.png\" alt=\"-\" class=\"wp-image-80989\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">The K8s agent: zero-config service discovery<\/h3>\n\n\n\n<p>A small (~50m CPU, &lt;150 MB memory) push-based agent runs in each of your clusters. It watches the Kubernetes API, samples <code>metrics-server<\/code> for CPU\/memory aggregates, and watches GitOps CRDs (ArgoCD <code>Applications<\/code>, Flux <code>Kustomizations<\/code> and <code>HelmReleases<\/code>). Every 30 seconds it pushes a batched, read-only summary to the Shoehorn API.<\/p>\n\n\n\n<p>What this gives you, with literally zero config beyond installing the agent:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Every workload in every cluster shows up in the catalog automatically. New services appear within minutes. Removed services disappear.<\/li>\n\n\n\n<li>Team ownership is inferred from namespace labels (e.g. <code>team: payments-team<\/code>), so there&#8217;s no separate manifest to maintain.<\/li>\n\n\n\n<li>GitOps state is tied to catalog entities, so &#8220;is this service in sync?&#8221; is a property of the entity, not a separate ArgoCD-tab dance.<\/li>\n\n\n\n<li>Because it&#8217;s push-based, the platform never holds kubeconfig credentials for your clusters. Cluster credentials never leave the cluster. (This is a real benefit for SOC 2 \/ HIPAA \/ generally paranoid environments.)<\/li>\n\n\n\n<li>Multi-cluster works out of the box. One agent per cluster, all reporting into the same catalog. The same service running across three clusters appears as a single entity, with visibility into all clusters it&#8217;s running in. No duplicate entries to deduplicate by hand.<\/li>\n<\/ul>\n\n\n\n<p>For richer metadata than labels can carry, add a single annotation pointing at an entity file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">annotations:\n  # Relative path, resolved against the repo URL Shoehorn already knows from ArgoCD\/Flux\/crawler\n  shoehorn.dev\/entityFile: \".shoehorn\/catalog.yaml\"\n\n  # Full URL, when the file lives in a different repo\n  shoehorn.dev\/entityFile: \"https:\/\/github.com\/acme\/shared-configs\/blob\/main\/.shoehorn\/checkout.yaml\"\n\n  # Multi-service file with a fragment selector\n  shoehorn.dev\/entityFile: \".shoehorn\/platform.yaml#checkout-api\"<\/code><\/pre>\n\n\n\n<p>The file gives you full enrichment: relations, links, runbooks, and interfaces. Start with zero-config and add an entity file only when the extra detail is worth it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The CLI: the catalog from the terminal<\/h3>\n\n\n\n<p><code>shoehorn<\/code> is a single Go binary, available via Homebrew, Scoop, mise, or a direct download. It does what <code>gh<\/code> does for GitHub: gives the developers who live in their terminal the same product the UI offers, without context-switching.<\/p>\n\n\n\n<p>A flavour of what you can do without leaving your shell:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\"># Browse the catalog\nshoehorn get entities --owner payments-team\nshoehorn search \"payment\"\nshoehorn get entity payment-service --scorecard\n\n# Run a Forge workflow (scaffolding) from the terminal\nshoehorn forge molds\nshoehorn forge run new-go-service --inputs name=billing-api,team=payments-team\n\n# Declarative ops: apply\/diff\/delete catalog entities like kubectl\nshoehorn apply -f service.yaml\nshoehorn diff -f service.yaml\nshoehorn validate mold .\/my-mold<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/4-shoehorn-entities-1024x200.png\" alt=\"-\" class=\"wp-image-80990\" \/><\/figure>\n\n\n\n<p>The CLI is the same API surface the UI uses, so anything you can do interactively, you can script. CI\/CD pipelines and developer onboarding scripts use it the same way they use <code>kubectl<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Terraform: the install and the day-2 lifecycle<\/h3>\n\n\n\n<p>Terraform isn&#8217;t just for the initial install. The Shoehorn Terraform provider (separate repo: <code>terraform-provider-shoehorn<\/code>) lets you declare catalog entities, teams, ownership, scorecards, and Forge molds the same way you declare any other infrastructure. So your developer platform, the thing that describes your <em>organisation<\/em>, can be version-controlled, peer-reviewed, and rolled back like any other Terraform state.<\/p>\n\n\n\n<p>Combined with the install module, a partner or platform engineer can describe an entire Shoehorn deployment as code: the platform itself, the org structure, the scorecards, the discovery agent. One <code>terraform apply<\/code> from a fresh kubeconfig to a running, populated, scored, and owned developer platform.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The TL;DR<\/h3>\n\n\n\n<p><code>kubectl<\/code> is the tool for resources. ArgoCD is the tool for sync. Shoehorn is the tool for the things that span clusters, repos, and teams. The three sit at different layers and work fine together.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">MCP support: your developer platform, addressable from an IDE agent<\/h2>\n\n\n\n<p>Shoehorn includes a built-in <strong>Model Context Protocol<\/strong> server. Every instance exposes an MCP endpoint at <code>\/mcp<\/code>, with a public capability document at <code>\/.well-known\/mcp<\/code> for registries and IDE agents to auto-discover.<\/p>\n\n\n\n<p>Three things matter about this for self-hosted teams:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>The agent talks to <em>your<\/em> Shoehorn.<\/strong> When a developer asks Claude, Cursor, or any MCP-capable IDE, &#8220;find me the payment service&#8217;s runbook&#8221; or &#8220;which services on the payments team are below tier-2 reliability?&#8221;, that query goes straight to your Shoehorn instance. The catalog data, the team mappings, the scorecards: none of it leaves your infrastructure to answer the question. The AI provider sees the question and the answer, but the <em>index<\/em> lives entirely in your cluster.<\/li>\n\n\n\n<li><strong>Read-only by design.<\/strong> The v0 MCP surface is annotated <code>readOnlyHint: true<\/code> for every tool. Agents can search the catalog, fetch entity details, look up teams, query scorecards, and read docs. They cannot create, modify, or delete anything. We&#8217;ll add write tools deliberately, behind explicit scopes, when it&#8217;s clear they&#8217;re worth the surface area.<\/li>\n\n\n\n<li><strong>Bounded by personal access tokens.<\/strong> Each developer generates their own PAT scoped to <code>mcp:read<\/code> from <code>\/profile\/mcp<\/code>, with chosen expiry (30\/90\/365 days or never). The token is bcrypt-hashed at rest; the raw value is shown once. PATs scope to a tenant, so an agent acting on behalf of one tenant&#8217;s developer can never see another tenant&#8217;s data.<\/li>\n<\/ul>\n\n\n\n<p>The tool surface today covers catalog entities, teams, scorecards, repositories, workloads, security findings, dependencies, docs, and a few diagnostic tools (zombies, whoami). Enough to make an IDE agent genuinely useful for navigating an internal developer platform; not enough to let it create entities, run Forge molds, or change ownership.<\/p>\n\n\n\n<p>The transport is stateless Streamable HTTP, protocol revision <code>2025-11-25<\/code>. The agent on the other side is whichever one you already use. Create PAT token from \/profile\/<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/3-shoehorn-ide-configuration-1024x817.png\" alt=\"-\" class=\"wp-image-80991\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">One <code>terraform apply<\/code><\/h2>\n\n\n\n<p>Our Terraform modules at <a href=\"https:\/\/github.com\/shoehorn-dev\/terraform-shoehorn-modules\" target=\"_blank\" rel=\"noopener\">terraform-shoehorn-modules<\/a> turn the entire deployment into a single <code>terraform apply<\/code> against any Kubernetes cluster you have a kubeconfig for.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">module \"shoehorn\" {\n  source = \"..\/..\/modules\/kubernetes\"\n\n  domain            = var.domain\n  organization_name = var.organization_name\n  organization_slug = var.organization_slug\n  admin_email       = var.admin_email\n\n  # Auth\n  auth_provider = \"okta\"\n  auth_config = {\n    domain              = var.okta_domain\n    clientId            = var.okta_client_id\n    issuer              = var.okta_issuer\n    authorizationServer = \"default\"\n  }\n\n  # Credentials\n  credentials = {\n    postgres_password      = random_password.postgres_password.result\n    db_password            = random_password.db_password.result\n    jwt_secret             = random_password.jwt_secret.result\n    auth_encryption_key    = random_bytes.auth_encryption_key.base64\n    session_encryption_key = random_bytes.session_encryption_key.base64\n    valkey_password        = random_password.valkey_password.result\n    meilisearch_master_key = random_password.meilisearch_master_key.result\n    okta_client_secret     = var.okta_client_secret\n    okta_api_token         = var.okta_api_token\n  }\n\n  # Bootstrap + Agent\n  enable_bootstrap = var.shoehorn_api_key == \"\"\n  deploy_agent     = true\n  cluster_id       = var.cluster_id\n  cluster_name     = var.cluster_name\n\n  # Okta user\/group sync. The module wires `auth.okta.clientSecretRef.key` and\n  # `auth.okta.apiTokenSecretRef.key` automatically from the matching keys in\n  # the `credentials` map above \u2014 no helm_set entry needed for those.\n  helm_set = {\n    \"auth.orgdata.enabled\"          = \"true\"\n    \"auth.orgdata.providers[0]\"     = \"okta\"\n    \"auth.orgdata.primaryProvider\"  = \"okta\"\n  }\n\n  health_check_protocol = \"https\"\n}<\/code><\/pre>\n\n\n\n<p>That single block does the following, in order, with no manual steps in between:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Generates and stores all required secrets (database passwords, JWT signing key, encryption keys, search master key) using Terraform <code>random_password<\/code> and <code>random_bytes<\/code> providers. Nothing is checked into source control, and no human types a password.<\/li>\n\n\n\n<li>Installs the Shoehorn Helm chart with all five Go microservices, their dependencies, and ingress configuration.<\/li>\n\n\n\n<li>Configures the OIDC provider connection (Okta, Zitadel, or any compliant IdP) and seeds the initial admin user.<\/li>\n\n\n\n<li>Issues a short-lived bootstrap API key, uses it to register the Kubernetes agent for service auto-discovery, then expires it. No long-lived bootstrap credentials hang around.<\/li>\n\n\n\n<li>Deploys the Kubernetes agent into the same cluster, which immediately starts populating the service catalog.<\/li>\n<\/ol>\n\n\n\n<p>The output is a working Shoehorn instance reachable at the domain you configured, with the catalog already showing whatever&#8217;s running in your cluster.<\/p>\n\n\n\n<p>For a cloud partner, this means a customer can request &#8220;give me a developer platform&#8221;, and the answer is one Terraform apply against a freshly provisioned managed Kubernetes cluster. There&#8217;s no bespoke handoff between provisioning the infrastructure and provisioning the platform. From &#8220;I want this&#8221; to &#8220;it&#8217;s running&#8221; is the same workflow you already use for everything else in your stack.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why this matters for sovereignty<\/h2>\n\n\n\n<p>The deployment-mechanics part is interesting because it&#8217;s <em>uninteresting<\/em>. There&#8217;s no clever deployment story to tell. You have a Kubernetes cluster (anywhere, on any provider, in any country) and Shoehorn deploys onto it. The cluster, the database storage, the object storage for backups, the OIDC provider: all of that lives wherever you put your cluster. The platform doesn&#8217;t depend on a SaaS control plane and doesn&#8217;t ship telemetry. The one outbound call in normal operation is a periodic license check, which we cover below.<\/p>\n\n\n\n<p>This matters for sovereignty because sovereignty is mostly a property of <em>where the data sits and who can compel access to it<\/em>, not a property of the software itself. A self-hosted platform on infrastructure under your jurisdiction is sovereign. A hosted platform &#8220;with a European region&#8221; generally isn&#8217;t, because the company hosting it is subject to whatever legal regime applies to the parent.<\/p>\n\n\n\n<p>By making Shoehorn fully self-hostable, with a deployment that takes one apply, we&#8217;re trying to remove the operational excuse that pushes teams towards the hosted option even when sovereignty matters to them. &#8220;We&#8217;d love to self-host, but we don&#8217;t have the bandwidth&#8221; is a real constraint. One <code>terraform apply<\/code> is a reasonable answer to it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What we deliberately don&#8217;t do, and what we&#8217;re upfront about<\/h2>\n\n\n\n<p>A few things we left out, on purpose:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No analytics or behavioural telemetry.<\/strong> The platform runs entirely inside your cluster. There&#8217;s no analytics endpoint we control. We don&#8217;t track which pages your users visit, which services they look at, or how long they spend in the app. None of that data ever leaves your infrastructure.<\/li>\n\n\n\n<li><strong>No SaaS control plane.<\/strong> Some &#8220;self-hosted&#8221; platforms still depend on SaaS platforms for authentication, feature flags, and &#8220;premium features.&#8221; Shoehorn doesn&#8217;t. The only external service the platform talks to in normal operation is your OIDC provider, whichever one you choose.<\/li>\n<\/ul>\n\n\n\n<p>One thing we do keep, which is worth being explicit about:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>A license check.<\/strong> Shoehorn periodically validates its license against our endpoint. The check carries the license identifier and an aggregate count, nothing else: no user data, no service names, no telemetry. Self-hosted instances continue to function during outages or air-gapped operation, and we publish exactly what&#8217;s sent. If your threat model genuinely requires zero outbound traffic, talk to us about an offline-license arrangement.<\/li>\n\n\n\n<li><strong>An SDK and an <\/strong>addon marketplace are in the works. The goal is to let the community extend the platform without compromising security. Each addon runs in a WebAssembly sandbox within your cluster, so third-party code can&#8217;t access anything beyond what the SDK exposes. Config and secrets are injected after install, never bundled with the addon and never committed to its repo. We&#8217;ve already built a JIRA sync add-on as the initial reference implementation. Shoehorn-built addons ship first, with a community marketplace to follow for discovery. Addons run locally and keep working offline, whether or not you can reach the marketplace.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s next<\/h2>\n\n\n\n<p>A short take on where we&#8217;re going. No dates, because we&#8217;d rather ship and announce than promise and slip. The order is up for negotiation; we plan the roadmap with customers, not at them. If something below matters to you and isn&#8217;t there yet, that&#8217;s a conversation we want to have.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>More authentication providers.<\/strong> Okta and Zitadel work today. Keycloak, Authelia, Authentik, and SAML\/Entra are next. The self-host audience generally has opinions about their IdP, and we&#8217;d rather meet you where you are than push you to ours.<\/li>\n\n\n\n<li><strong>More Git providers.<\/strong> GitHub is solid; GitLab (cloud and self-managed) and Gitea are next in line. For sovereignty-conscious teams running their own Git, we want to make sure Shoehorn enriches <em>your<\/em> repositories, not just ones hosted in the US.<\/li>\n\n\n\n<li><strong>More Forge molds out of the box.<\/strong> The scaffolding engine works with any mold you author, but most teams don&#8217;t want to start from a blank repo. We&#8217;re building a library of opinionated molds.<\/li>\n\n\n\n<li><strong>More addons.<\/strong> The JIRA sync is the first; more Shoehorn-built addons next, then community submissions through the marketplace. All sandboxed, all local.<\/li>\n<\/ul>\n\n\n\n<p>The bigger direction, and the one we care most about, is <strong>making the platform itself more intelligent, invisibly.<\/strong><\/p>\n\n\n\n<p>Not a chatbot bolted to a sidebar. We&#8217;re building intelligence into the platform itself, surfacing proactive insights in the workflows you already use. The MCP server is the first piece. More as it ships.<\/p>\n\n\n\n<p>Every bit of this stays consistent with the sovereignty posture. Inference runs inside your infrastructure, against your data. Where a model is involved, the customer chooses where it runs and what gets sent. No mandatory cloud AI dependency. &#8220;Intelligent&#8221; and &#8220;self-hosted&#8221; don&#8217;t have to be a contradiction.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Coming soon: one-click install for UpCloud customers<\/h2>\n\n\n\n<p>UpCloud customers will be able to deploy Shoehorn onto their managed Kubernetes clusters with a single click: the same <code>terraform apply<\/code> shown above, wrapped in the kind of one-step UX that makes &#8220;spin up a developer platform&#8221; indistinguishable from &#8220;spin up a database.&#8221; One workflow provisions the cluster, runs the module, generates secrets, configures ingress, registers the agent, and populates the catalog.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Have a look<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Demo:<\/strong> https:\/\/demo.shoehorn.dev<\/li>\n\n\n\n<li><strong>Docs:<\/strong> https:\/\/shoehorn.dev<\/li>\n<\/ul>\n\n\n\n<p>Open, self-hostable, and built so you don&#8217;t have to build the portal before you can use it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We built Shoehorn out of frustration. Some internal developer portals act like frameworks: you install one, then spend a quarter sifting through unmaintained community plugins, [&hellip;]<\/p>\n","protected":false},"author":15,"featured_media":81005,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_relevanssi_hide_post":"","_relevanssi_hide_content":"","_relevanssi_pin_for_all":"","_relevanssi_pin_keywords":"","_relevanssi_unpin_keywords":"","_relevanssi_related_keywords":"","_relevanssi_related_include_ids":"","_relevanssi_related_exclude_ids":"","_relevanssi_related_no_append":"","_relevanssi_related_not_related":"","_relevanssi_related_posts":"733,187,178,118,868,3627","_relevanssi_noindex_reason":"Blocked by a filter function","footnotes":""},"categories":[94,28,103],"tags":[],"class_list":["post-4003","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-guest-stories","category-long-reads","category-open-source"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/posts\/4003","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=4003"}],"version-history":[{"count":4,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/posts\/4003\/revisions"}],"predecessor-version":[{"id":6914,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/posts\/4003\/revisions\/6914"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=4003"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/categories?post=4003"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tags?post=4003"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}