<?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>Forem: gyorgy</title>
    <description>The latest articles on Forem by gyorgy (@gyorgy).</description>
    <link>https://forem.com/gyorgy</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%2F3868028%2F54d5507c-1a65-4f3f-917d-7357374a763f.JPG</url>
      <title>Forem: gyorgy</title>
      <link>https://forem.com/gyorgy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gyorgy"/>
    <language>en</language>
    <item>
      <title>MCP annotations are a UX layer, not a security layer</title>
      <dc:creator>gyorgy</dc:creator>
      <pubDate>Tue, 05 May 2026 14:18:58 +0000</pubDate>
      <link>https://forem.com/gyorgy/mcp-annotations-are-a-ux-layer-not-a-security-layer-mdh</link>
      <guid>https://forem.com/gyorgy/mcp-annotations-are-a-ux-layer-not-a-security-layer-mdh</guid>
      <description>&lt;p&gt;When the Model Context Protocol added tool annotations like &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;, and &lt;code&gt;idempotentHint&lt;/code&gt;, a lot of MCP server authors and host implementers read them as a permission system. The mental model goes something like: a tool declares itself destructive, the host sees that, and the host either prompts the user or refuses outright. Annotations as enforcement, the way file permissions work in a Unix filesystem.&lt;/p&gt;

&lt;p&gt;That's not what they are. A tool annotation is a string the server author typed into a tool definition. The model sees it, the host sees it, and they can use it for confirmation prompts or sorting or color coding. Nothing in the protocol verifies the annotation is true. A server can declare &lt;code&gt;readOnlyHint: true&lt;/code&gt; on a tool that drops your production database, and the protocol won't notice. The host can choose to trust the annotation or not, but the trust is a policy decision the host makes about the server, not something the protocol provides.&lt;/p&gt;

&lt;p&gt;This distinction matters because the annotation system is being asked to carry weight it wasn't designed to carry. Two active spec proposals (&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1862" rel="noopener noreferrer"&gt;SEP-1862&lt;/a&gt; and &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1913" rel="noopener noreferrer"&gt;SEP-1913&lt;/a&gt;) extend the annotation surface in useful ways. Neither of them changes what annotations fundamentally are. They make a UX layer better. They do not turn it into a security layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What annotations actually are
&lt;/h2&gt;

&lt;p&gt;Annotations are server-declared hints. The server author writes them into the tool definition, the server sends them to the client in &lt;code&gt;tools/list&lt;/code&gt;, and that's the entire chain of custody. There is no signature, no third-party verification, no model-side analysis of what the tool actually does. The annotation is exactly as trustworthy as the server that produced it.&lt;/p&gt;

&lt;p&gt;The MCP specification is explicit about this. From the &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/schema" rel="noopener noreferrer"&gt;schema documentation&lt;/a&gt;: "All properties in ToolAnnotations are hints. They are not guaranteed to provide a faithful description of tool behavior... Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers." That language is in the spec because the working group knows annotations are forgeable.&lt;/p&gt;

&lt;p&gt;Justin Spahr-Summers, one of the MCP co-creators, raised the obvious question during the original review of the annotation system: if a client knows the annotations can't be trusted, what's the point of having them? It's the right question and the spec hasn't really answered it. The working answer in practice is that annotations are useful for two things. First, hosts can build better UX on top of them when the server is trusted (skip the confirmation prompt for a tool that declares itself read-only, render destructive tools in a different color, sort tools so safer ones are surfaced first). Second, hosts can use annotations as one signal among many when scoring how much to scrutinize a tool call.&lt;/p&gt;

&lt;p&gt;Neither of those is enforcement. Both assume the host has already decided the server is honest. The annotation tells the host how to render the tool's intent, not whether to allow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two SEPs in flight
&lt;/h2&gt;

&lt;p&gt;Two annotation-related proposals are currently working through the MCP spec process, both authored or co-authored by Sam Morrow at GitHub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1862" rel="noopener noreferrer"&gt;SEP-1862&lt;/a&gt; (Tool Resolution) addresses a real problem with static annotations: a single tool that takes an &lt;code&gt;action&lt;/code&gt; argument and behaves differently based on its value has to declare itself destructive at all times, because the static annotation has to cover the worst case. A &lt;code&gt;manage_files&lt;/code&gt; tool that supports both &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; operations is forced to look as dangerous as its most dangerous mode, even on read calls. The fix is a new &lt;code&gt;tools/resolve&lt;/code&gt; method, inspired by LSP's &lt;code&gt;codeAction/resolve&lt;/code&gt; pattern. Before invoking the tool, the client asks the server: given these specific arguments, what are the real annotations? The server returns refined metadata for that call. Multi-action tools become viable again without sacrificing UX accuracy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1913" rel="noopener noreferrer"&gt;SEP-1913&lt;/a&gt; (Trust and Sensitivity Annotations), co-authored with OpenAI, works on a different axis. Where existing annotations describe what a tool does, SEP-1913 adds annotations that describe what the data flowing through a tool means. New fields like &lt;code&gt;sensitiveHint&lt;/code&gt; (low/medium/high), &lt;code&gt;privateHint&lt;/code&gt;, &lt;code&gt;maliciousActivityHint&lt;/code&gt;, and &lt;code&gt;attribution&lt;/code&gt; let servers mark returned data with trust and sensitivity metadata, and let that metadata propagate through an agent session so a host can enforce policies like "do not send data marked private to tools marked open-world."&lt;/p&gt;

&lt;p&gt;Both proposals fill genuine gaps. SEP-1862 unblocks a tool design pattern that was effectively forbidden by static annotations. SEP-1913 extends the annotation surface from what tools do to what data they handle, which is the right direction if you care about prompt injection and exfiltration.&lt;/p&gt;

&lt;p&gt;What neither proposal changes is the trust model. SEP-1862's resolved annotations are still server-declared. SEP-1913's data annotations are still server-declared. A server that lies in &lt;code&gt;tools/list&lt;/code&gt; can lie just as easily in &lt;code&gt;tools/resolve&lt;/code&gt; or in a &lt;code&gt;sensitiveHint&lt;/code&gt; field on returned content. The proposals make honest servers more expressive. They do not make dishonest servers detectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for MCP server design today
&lt;/h2&gt;

&lt;p&gt;If annotations are a UX layer, design your server so the UX layer stays accurate without depending on protocol-level enforcement.&lt;/p&gt;

&lt;p&gt;The first decision is tool granularity. A multi-action tool with an &lt;code&gt;action&lt;/code&gt; argument forces a worst-case static annotation, which means honest hosts will over-prompt and well-tuned models will steer around the tool because it looks dangerous. Until SEP-1862 lands, separate tools per action keep static annotations honest. One tool reads, one tool lists, one tool removes. Each declares its real shape and the annotation is true at all times. This costs you a few more tool definitions and saves the host from making bad UX decisions on your behalf.&lt;/p&gt;

&lt;p&gt;The second decision is how to use the existing annotation fields. The boolean grid (&lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;, &lt;code&gt;idempotentHint&lt;/code&gt;, &lt;code&gt;openWorldHint&lt;/code&gt;) is independent flags rather than ordered tiers, but in practice tools cluster into three groups. Read-only tools (&lt;code&gt;readOnlyHint: true&lt;/code&gt;). Mutating but recoverable tools (&lt;code&gt;readOnlyHint: false, destructiveHint: false&lt;/code&gt;). Destructive tools (&lt;code&gt;readOnlyHint: false, destructiveHint: true&lt;/code&gt;). Treating these as a tier internally simplifies host policy, even though the protocol doesn't enforce the structure. It also makes it obvious which tier a new tool belongs to when you add one, which matters at scale.&lt;/p&gt;

&lt;p&gt;The third decision is what to do about the trust gap. The honest answer is that the protocol can't close it for you, so you close it elsewhere. Sandboxed execution, infrastructure-level egress controls, and third-party scanners (&lt;a href="https://github.com/snyk/agent-scan" rel="noopener noreferrer"&gt;Snyk's Agent Scan&lt;/a&gt; is one example) sit outside the protocol and verify or constrain what tools actually do, regardless of what they claim. If your MCP server runs in a context where any of those layers exist, lean on them. The annotations on your tools should be honest, but the security boundary lives somewhere else.&lt;/p&gt;

&lt;p&gt;What you should not do is treat annotation correctness as the security boundary. A server author who annotates carefully and a server author who lies look identical to the protocol. If your design assumes the host can tell them apart through annotations alone, you have a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual security layer lives outside MCP
&lt;/h2&gt;

&lt;p&gt;Once you accept that annotations are a UX layer, the question of where security actually lives becomes easier to answer. It lives in three places, none of them in the protocol.&lt;/p&gt;

&lt;p&gt;The first is host-level policy on which servers to trust. The host decides which MCP servers it accepts tools from, what scopes those servers operate under, and what the user has approved. That's where the real allow/deny decision happens. Annotations help the host build clearer prompts and better defaults, but the host is the one accepting or rejecting the tool call.&lt;/p&gt;

&lt;p&gt;The second is infrastructure-level enforcement. Sandboxed execution, network egress rules, filesystem permissions, container boundaries. These don't care what a tool's annotations say. A tool that claims to be read-only but tries to write outside its sandbox is stopped by the sandbox, not by the annotation. For any MCP server doing real work in production, this layer is where deletion, exfiltration, and lateral movement actually get prevented.&lt;/p&gt;

&lt;p&gt;The third is third-party verification. Scanners that examine MCP server code or behavior independently of what the server claims. &lt;a href="https://github.com/snyk/agent-scan" rel="noopener noreferrer"&gt;Snyk's Agent Scan&lt;/a&gt; is one example of this category, and more will appear as the ecosystem matures. These tools occupy the space the protocol can't, because by definition they treat the server as untrusted and verify rather than trust.&lt;/p&gt;

&lt;p&gt;None of this makes annotations useless. Annotations let honest servers communicate intent, let hosts build interfaces that match that intent, and give users the right amount of friction at the right moments. SEP-1862 will make that signal sharper for multi-action tools. SEP-1913 will extend it to the data flowing through tools. Both are worth shipping.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Migrating off AWS App Runner before the April 30 deadline</title>
      <dc:creator>gyorgy</dc:creator>
      <pubDate>Tue, 14 Apr 2026 14:11:34 +0000</pubDate>
      <link>https://forem.com/gyorgy/migrating-off-aws-app-runner-before-the-april-30-deadline-5g8m</link>
      <guid>https://forem.com/gyorgy/migrating-off-aws-app-runner-before-the-april-30-deadline-5g8m</guid>
      <description>&lt;p&gt;AWS is shutting the door on App Runner for new customers effective April 30, 2026. If you're running production workloads on it, existing apps keep working for now, but there are no new features coming, and "maintenance mode" at AWS historically means "start planning your migration."&lt;/p&gt;

&lt;p&gt;I just finished a migration off App Runner for a production Next.js frontend, and wanted to write down what I learned in case it's useful to anyone else facing the same deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;p&gt;AWS officially recommends &lt;strong&gt;ECS Express Mode&lt;/strong&gt; as the direct App Runner replacement. It's a newer single-resource abstraction that auto-provisions an ECS cluster, service, ALB, security groups, auto-scaling, and CloudWatch logging. One Terraform resource, one deploy, done.&lt;/p&gt;

&lt;p&gt;The other options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard ECS Fargate&lt;/strong&gt;. More moving parts, years of battle-testing, full control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda + API Gateway&lt;/strong&gt;. True scale-to-zero, good for infrequent API traffic, cold starts on anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightsail containers&lt;/strong&gt;. Simpler than ECS, cheaper for small workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Run&lt;/strong&gt;. If you're open to leaving AWS, this is genuinely the best container-in-a-box experience on any cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fly.io / Render / Railway&lt;/strong&gt;. PaaS experience outside AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our use case (production Next.js behind CloudFront with a real VPC, Kong gateway, and backend services on the same infrastructure), ECS Fargate was the natural fit. Express Mode looked appealing on paper, but I went with standard Fargate instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not ECS Express Mode
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Terraform bug.&lt;/strong&gt; The &lt;code&gt;aws_ecs_express_gateway_service&lt;/code&gt; resource had an open issue (hashicorp/terraform-provider-aws#45792, "Provider produced inconsistent result after apply") that would have blocked deploys. Fixable with workarounds, but not something I wanted to own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Managed abstraction" fatigue.&lt;/strong&gt; App Runner was also supposed to be the easy path. It lasted four years before being sidelined. Express Mode is newer than App Runner was when I first used it. I wasn't willing to bet a second production frontend on another abstraction that might get sunset in 18 months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. ALB duplication.&lt;/strong&gt; Express Mode auto-creates its own ALB. If you already have an ALB for other services (like I did for a Kong gateway routing backend services), you end up paying for two. Around $16/month extra for the overlap. Not huge, but annoying and unnecessary.&lt;/p&gt;

&lt;p&gt;Standard ECS Fargate uses the ALB you already have. Same pattern as every other service in the cluster. Boring, predictable, stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the migration actually looked like
&lt;/h2&gt;

&lt;p&gt;The architecture ended up like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  ↓
CloudFront (caching + WAF)
  ↓ X-Origin-Verify header
ALB (port 443, host-based routing)
  ↓                    ↓
Next.js target      Kong target
group               group
  ↓                    ↓
ECS Fargate         Kong gateway
(Next.js)              ↓
                    Backend services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js containers run in private VPC subnets. ALB listener rules use host-based routing to split frontend traffic (&lt;code&gt;example.com&lt;/code&gt; → Next.js target group) from API traffic (any host + X-Origin-Verify header → Kong target group). CloudFront in front for caching, SSL, and WAF.&lt;/p&gt;

&lt;p&gt;For origin protection, I stuck with &lt;code&gt;X-Origin-Verify&lt;/code&gt; header validation on the ALB rule. The AWS-managed CloudFront prefix list is a cleaner option (allow only CloudFront IPs at the security group level) but it's more moving parts and one more thing to update when AWS changes its prefix list. The header check was good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Health checks.&lt;/strong&gt; Next.js needs a &lt;code&gt;/health&lt;/code&gt; endpoint returning 200 for ALB target group health checks. This is obvious in retrospect but it was our first failed deploy. Add it to your &lt;code&gt;app/health/route.ts&lt;/code&gt; before you migrate, not during.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-phase deploy.&lt;/strong&gt; The App Runner + CloudFront setup I had was a two-phase deploy: Terraform creates App Runner, CLI collects the URL, Terraform runs again with the URL as a CloudFront origin. With ECS behind an ALB that already exists at plan time, this goes away. One &lt;code&gt;terraform apply&lt;/code&gt;, no two-phase dance. Genuinely nicer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private subnets from the start.&lt;/strong&gt; App Runner services are publicly routable on the internet, with WAF-only protection and no network-level isolation. ECS Fargate in private subnets gives you proper network boundaries. Don't skip this. Put your container in private subnets with no public IP, only allow ingress from the ALB security group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-scaling.&lt;/strong&gt; Express Mode gives you auto-scaling for free. Standard Fargate requires configuring target-tracking scaling policies yourself. One extra Terraform resource, but you have actual control over what the scaling metric is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about scale-to-zero?
&lt;/h2&gt;

&lt;p&gt;This is the pain point for everyone moving off App Runner. Standard Fargate does not scale to zero. You always pay for at least one running task. If your workload has long idle periods, this is a real cost difference.&lt;/p&gt;

&lt;p&gt;For production workloads this is usually fine (you want at least one container warm anyway). For dev/staging environments or low-traffic side projects, you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run on GCP&lt;/strong&gt;. Actual scale-to-zero, sub-second cold starts, no ALB needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda + API Gateway&lt;/strong&gt;. Scale-to-zero, but cold starts hurt if your app isn't designed for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled shutdowns&lt;/strong&gt;. &lt;code&gt;eventbridge&lt;/code&gt; rules to scale the ECS service to 0 at night, back to 1 in the morning. Crude but effective for dev environments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your app is a very low traffic fastapi backend (as in the Reddit thread that prompted this article), honestly, Cloud Run is probably the right answer. AWS just doesn't have a real equivalent right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I do it again?
&lt;/h2&gt;

&lt;p&gt;Yeah, for a production workload with an existing VPC and other services, the standard Fargate path was the right call. The migration was not fun but the result is cleaner than App Runner. Single-phase deploys, private networking, no dependency on a deprecated service.&lt;/p&gt;

&lt;p&gt;If I were starting fresh with a brand new single service and no existing infrastructure, I'd look harder at Cloud Run or fly.io. AWS's container story below ECS is just not compelling anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tsdevstack angle
&lt;/h2&gt;

&lt;p&gt;I build a multi-cloud TypeScript framework called &lt;a href="https://tsdevstack.dev" rel="noopener noreferrer"&gt;tsdevstack&lt;/a&gt; that generates production infrastructure from a config file. The App Runner to ECS Fargate migration above is what shipped in v0.2.0. Framework users who were deploying Next.js frontends via App Runner can now re-run &lt;code&gt;infra:deploy&lt;/code&gt; and the framework handles the migration automatically.&lt;/p&gt;

&lt;p&gt;One thing worth mentioning given the scale-to-zero discussion above: tsdevstack implements scale-to-zero on AWS for services that set &lt;code&gt;minInstances: 0&lt;/code&gt; in config. Since ECS Fargate doesn't have native scale-to-zero, the framework generates a three-layer mechanism: a CloudWatch alarm scales the service to zero when idle (CPU below 5% for 15 minutes), and a wake-up Lambda spins it back up when the first request hits the ALB and returns 502. Kong catches the 502, fires the wake-up call, and returns a 503 with &lt;code&gt;Retry-After: 30&lt;/code&gt; so the client retries automatically. Cold start is around 30-60 seconds, which is significant compared to Cloud Run or Container Apps, but it's real scale-to-zero on AWS and it works. Kong itself stays at &lt;code&gt;minInstances &amp;gt;= 1&lt;/code&gt; so there's always something to trigger the wake-up.&lt;/p&gt;

&lt;p&gt;If you're tired of writing Terraform by hand for every AWS migration AWS forces on you, take a look. &lt;a href="https://tsdevstack.dev" rel="noopener noreferrer"&gt;Docs here&lt;/a&gt;, repo at &lt;a href="https://github.com/tsdevstack" rel="noopener noreferrer"&gt;github.com/tsdevstack&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: aws, terraform, devops, cloud&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>I built a TypeScript framework that generates your entire cloud infrastructure</title>
      <dc:creator>gyorgy</dc:creator>
      <pubDate>Wed, 08 Apr 2026 15:18:57 +0000</pubDate>
      <link>https://forem.com/gyorgy/i-built-a-typescript-framework-that-generates-your-entire-cloud-infrastructure-1392</link>
      <guid>https://forem.com/gyorgy/i-built-a-typescript-framework-that-generates-your-entire-cloud-infrastructure-1392</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; tsdevstack is an open-source TypeScript microservices framework. You write a config file and application code. It generates Terraform, Docker, Kong gateway routes, CI/CD pipelines, secrets, and observability — across GCP, AWS, and Azure. One command deploys the whole stack.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6MJ4PPPjxH8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every TypeScript project I shipped to production followed the same pattern. Write the application code in a week. Spend the next month wiring up infrastructure.&lt;/p&gt;

&lt;p&gt;Terraform for the cloud resources. Docker for local dev. Kong or some other gateway for routing. JWT auth boilerplate. Secrets management across environments. CI/CD pipelines. Observability. WAF rules. SSL certificates. Health checks. Database migrations. And then the same dance for staging and production.&lt;/p&gt;

&lt;p&gt;The application code was the easy part. Everything around it took 10x longer.&lt;/p&gt;

&lt;p&gt;I tried the existing options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heroku-style platforms&lt;/strong&gt; hide too much. The moment you need a WAF, a custom gateway, or VPC isolation, you're stuck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulumi/CDK/Terraform modules&lt;/strong&gt; are flexible but you still write and maintain all of it. And you write it differently for each cloud provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates and starters&lt;/strong&gt; get you a working hello-world but rot the moment you customise them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something in between. A framework that owned the infrastructure layer entirely — generated, managed, deployed — but stayed out of the way of the application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure as Framework.&lt;/strong&gt; You write TypeScript application code and one config file. The framework generates everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tsdevstack/cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scaffolds a monorepo with NestJS backends, Next.js frontends, a Kong API gateway, Postgres, Redis, and observability. Everything wired together, ready to run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local development matches production. Same gateway, same database engine, same auth flow, same observability stack. No "works on my machine" gap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsdevstack cloud:init &lt;span class="nt"&gt;--gcp&lt;/span&gt;
npx tsdevstack infra:init &lt;span class="nt"&gt;--env&lt;/span&gt; dev
npx tsdevstack infra:deploy &lt;span class="nt"&gt;--env&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provisions the full production stack: VPC, managed Postgres, Redis, container registry, Cloud Run services, API gateway, load balancer, WAF, SSL certificates, observability. From a single config file.&lt;/p&gt;

&lt;p&gt;The same flow works on AWS (ECS Fargate) and Azure (Container Apps). Same framework, same patterns, same commands. No rewriting infrastructure when you switch providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Application layer&lt;/strong&gt; — NestJS backends, Next.js frontends, Rsbuild SPAs. Auto-generated TypeScript API clients with DTOs as separated imports — both frontend and backend apps consume the same type-safe library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API gateway&lt;/strong&gt; — Kong routes auto-generated from your OpenAPI specs. JWT validation, rate limiting, CORS, bot detection. Fully customisable when you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background processing&lt;/strong&gt; — BullMQ job queues with detached workers running in separate containers. Scale independently from API services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object storage&lt;/strong&gt; — Add buckets with &lt;code&gt;add-bucket-storage&lt;/code&gt;. MinIO locally, S3/GCS/Azure Blob in production. Unified &lt;code&gt;StorageModule&lt;/code&gt; with pre-signed URLs and per-provider adapters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async messaging&lt;/strong&gt; — Inter-service pub/sub via Redis Streams. Consumer groups, dead letter queues, retry logic. No new infrastructure — runs on the same Redis instance as caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; — JWT token management, protected routes, session handling, email confirmation. Bring your own OIDC or use the built-in auth service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; — Local secrets generated automatically for development. Cloud secrets managed separately and pushed to the cloud provider's Secret Manager. Environment isolation, scoped per service. Works with Secret Manager on all three providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt; — Prometheus metrics, Grafana dashboards, distributed tracing with Jaeger, structured logging. Configured from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — Generated Terraform for GCP, AWS, and Azure. VPC/VNet, managed databases, Redis, container orchestration, load balancers, WAF, SSL, CDN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI/CD&lt;/strong&gt; — Generated GitHub Actions workflows. OIDC authentication, per-service deploys, environment selection. No secrets in your repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance&lt;/strong&gt; — SOC 2, ISO 27001, GDPR technical controls built in. Encryption at rest and in transit, network isolation, zero-credential runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;The framework manages a &lt;code&gt;config.json&lt;/code&gt; for your project structure — you don't edit it by hand, you modify it through commands like &lt;code&gt;add-service&lt;/code&gt;, &lt;code&gt;add-bucket-storage&lt;/code&gt;, &lt;code&gt;add-messaging-topic&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;config.json&lt;/code&gt; ends up looking like this:&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;"projectName"&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-saas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cloud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gcp"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"auth-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nestjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hasDatabase"&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;"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;"frontend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nextjs"&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;"storage"&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;"buckets"&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;"uploads"&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;When you run &lt;code&gt;npx tsdevstack sync&lt;/code&gt;, the framework reads the config and generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-compose.yml&lt;/code&gt; with all the services, dependencies, and health checks&lt;/li&gt;
&lt;li&gt;Kong gateway config from OpenAPI specs&lt;/li&gt;
&lt;li&gt;Local secrets in &lt;code&gt;.env&lt;/code&gt; files per service&lt;/li&gt;
&lt;li&gt;Database initialization scripts&lt;/li&gt;
&lt;li&gt;Service stubs if you added new ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write the &lt;code&gt;infrastructure.json&lt;/code&gt; directly for cloud-specific settings (domains, scaling, environments).&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;"environments"&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;"dev"&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;"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;"auth-service"&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;"minInstances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"maxInstances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512Mi"&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;"frontend"&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;"minInstances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"maxInstances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1Gi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.example.com"&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;When you run &lt;code&gt;npx tsdevstack infra:deploy&lt;/code&gt;, it generates Terraform for your chosen provider and applies it. The framework owns the Terraform, you don't write it, you don't maintain it.&lt;/p&gt;

&lt;p&gt;The escape hatch is intentional. Custom Kong config? Drop in your own. Need a Terraform resource the framework doesn't generate? Add it as a side file. Need a cloud-native service the framework doesn't wrap? Use the SDK directly. The framework isn't a cage — it's a starting point that handles 95% of cases and gets out of your way for the other 5%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why three clouds?
&lt;/h2&gt;

&lt;p&gt;Vendor lock-in is real but slow-moving. You don't switch clouds because you want to — you switch because acquisition, pricing change, region requirements, or a customer with an immovable preference forces you to. When that happens, rewriting infrastructure is brutal.&lt;/p&gt;

&lt;p&gt;tsdevstack generates the equivalent infrastructure on GCP (Cloud Run + Cloud SQL + Memorystore), AWS (ECS Fargate + RDS + ElastiCache), and Azure (Container Apps + Azure Database for PostgreSQL + Azure Cache for Redis). Same application code, same config file, different generated Terraform. Switching providers is a config change and a redeploy.&lt;/p&gt;

&lt;p&gt;No abstraction layer trying to hide the differences between clouds. Each provider gets a native, idiomatic implementation. The framework handles the translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about AI agents?
&lt;/h2&gt;

&lt;p&gt;There's a built-in MCP (Model Context Protocol) server with 54 tools for deploying, querying, and debugging your stack. Claude Code, Cursor, and VS Code Copilot can manage the infrastructure directly — and because the framework has strong conventions, the AI agent actually understands what it's doing instead of hallucinating CLI commands.&lt;/p&gt;

&lt;p&gt;Three permission tiers: SAFE_READ, CLOUD_MUTATE, CLOUD_DESTRUCTIVE. The agent always asks for permission before mutating anything. The MCP server is built into the CLI — no separate package, no extra setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Open source.&lt;/strong&gt; MIT license. Four packages on npm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@tsdevstack/cli&lt;/code&gt; — the CLI, infrastructure generation, deployment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tsdevstack/nest-common&lt;/code&gt; — shared NestJS modules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tsdevstack/cli-mcp&lt;/code&gt; — MCP server for AI agents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tsdevstack/react-bot-detection&lt;/code&gt; — React bot detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.2.0 just shipped&lt;/strong&gt; with object storage, async messaging, AWS App Runner → ECS Fargate migration (App Runner stops accepting new customers April 30), and a batch of WAF and observability improvements across all three providers.&lt;/p&gt;

&lt;p&gt;This is solo work. I'm a developer building this on the side. It started as the framework I wanted for my own projects and grew into something I think other people will find useful. The first users are showing up now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tsdevstack/cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs and guides: &lt;a href="https://tsdevstack.dev" rel="noopener noreferrer"&gt;tsdevstack.dev&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/tsdevstack" rel="noopener noreferrer"&gt;github.com/tsdevstack&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/tsdevstack" rel="noopener noreferrer"&gt;discord.gg/tsdevstack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback wanted. Bug reports wanted. Issues, ideas, complaints — all welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: typescript, nestjs, devops, opensource&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>nestjs</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
