<?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: Yuiko Koyanagi</title>
    <description>The latest articles on DEV Community by Yuiko Koyanagi (@toffy).</description>
    <link>https://dev.to/toffy</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636738%2Fe52866ed-1547-49e3-930c-4f54ea9467d3.jpeg</url>
      <title>DEV Community: Yuiko Koyanagi</title>
      <link>https://dev.to/toffy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toffy"/>
    <language>en</language>
    <item>
      <title>Best AI Gateways with Built-In Observability &amp; Governance (2026)</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/toffy/best-ai-gateways-with-built-in-observability-governance-2026-27dg</link>
      <guid>https://dev.to/toffy/best-ai-gateways-with-built-in-observability-governance-2026-27dg</guid>
      <description>&lt;p&gt;An LLM feature usually starts with one &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; in a &lt;code&gt;.env&lt;/code&gt; file. It stops being that once a second provider shows up, then a second team, and then someone asks why inference spend doubled last month and nobody can break the number down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI gateway is
&lt;/h2&gt;

&lt;p&gt;An AI gateway is a proxy between your applications and the model providers you call. Instead of every service holding its own provider keys and its own retry logic, they all talk to one endpoint.&lt;/p&gt;

&lt;p&gt;Without a gateway, every service is on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐
│  chat-api    │──── OPENAI_API_KEY ──────────▶ OpenAI
├──────────────┤
│  summarizer  │──── ANTHROPIC_API_KEY ───────▶ Anthropic
├──────────────┤
│  batch-job   │──── OPENAI_API_KEY (copy) ───▶ OpenAI
└──────────────┘

Keys copied into every service. No shared view of cost.
A provider outage is handled — or not — three separate times.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With one in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐
│  chat-api    │──┐
├──────────────┤  │   ┌─────────────────────────┐      ┌───▶ OpenAI
│  summarizer  │──┼──▶│      AI GATEWAY         │──────┼───▶ Anthropic
├──────────────┤  │   │                         │      └───▶ Bedrock
│  batch-job   │──┘   │  • routing &amp;amp; failover   │
└──────────────┘      │  • virtual keys         │
   one base URL       │  • budgets, rate limits │
   one credential     │  • cost per request     │
                      │  • traces &amp;amp; metrics     │
                      └───────────┬─────────────┘
                                  │
                                  ▼
                     Prometheus / OpenTelemetry
                     → your existing dashboards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway can decide which provider serves each call. That gives you failover when one goes down, cost attributed per team, budgets that stop runaway spend, and traces in the dashboards you already use.&lt;/p&gt;

&lt;p&gt;Proxying is the easy part; every product does it. The differences are in that second list — and in how much of it you get before hitting a paywall.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore four AI gateways: &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, &lt;a href="https://www.litellm.ai/" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt;, &lt;a href="https://www.cloudflare.com/products/ai-gateway/" rel="noopener noreferrer"&gt;Cloudflare AI Gateway&lt;/a&gt;, and &lt;a href="https://vercel.com/ai-gateway" rel="noopener noreferrer"&gt;Vercel AI Gateway&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two families
&lt;/h2&gt;

&lt;p&gt;These four gateways split cleanly into two distinct categories, and this split matters more than any individual feature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted&lt;/strong&gt; (Bifrost, LiteLLM) — you run the gateway and keep its configuration, provider credentials, governance state, and logs on infrastructure you control. Prompts remain inside your network only when the upstream model endpoint does too. You also own the database, upgrades, backups, and on-call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed&lt;/strong&gt; (Cloudflare, Vercel) — the vendor runs it. Setup takes minutes, there is no infrastructure to manage, and both integrate tightly with their native platforms. You cannot self-host, and your prompts transit their network.&lt;/p&gt;

&lt;p&gt;That choice constrains everything that follows. Within each pair, the differences come down to emphasis:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Strongest at&lt;/th&gt;
&lt;th&gt;Main limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bifrost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Governance on a bare container; MCP gateway; low overhead&lt;/td&gt;
&lt;td&gt;Fewer providers than LiteLLM; HA, SSO, audit logs, and guardrails are Enterprise-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LiteLLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provider coverage; routing flexibility; external observability integrations&lt;/td&gt;
&lt;td&gt;Production topology is genuinely complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DLP and guardrails; visual dynamic routing; no infrastructure&lt;/td&gt;
&lt;td&gt;Account-scoped tokens; Response-side DLP buffers streaming responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer experience; modality breadth; provider-level routing&lt;/td&gt;
&lt;td&gt;Advanced governance is metered; no self-hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Self-hosted: Bifrost
&lt;/h2&gt;

&lt;p&gt;Apache 2.0, Go, in front of 1,000+ models across 23+ providers, with 7.7k stars &lt;a href="https://github.com/maximhq/bifrost/" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out: governance is running before you configure anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Telemetry, logging, and governance are &lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;auto-loaded built-ins&lt;/a&gt; — in the &lt;code&gt;maximhq/bifrost:latest&lt;/code&gt; image I pulled on 30 August 2026, seven internal plugins registered at startup, those three among them. What that means in practice: &lt;code&gt;/metrics&lt;/code&gt; serves Prometheus without adding an exporter, and the virtual key, team, budget, and customer endpoints answer immediately against the default SQLite store. Creating a key with a daily spend cap and a token rate limit is one call, no external database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:8080/api/governance/virtual-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "team-search",
    "budget": {"max_limit": 10.0, "reset_duration": "1d"},
    "rate_limit": {"token_max_limit": 1000, "token_reset_duration": "1m"}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;virtual key&lt;/a&gt; is the organising primitive: model access, &lt;a href="https://docs.getbifrost.ai/features/governance/budget-and-limits" rel="noopener noreferrer"&gt;budgets&lt;/a&gt; nested across key/team/customer, rate limits, routing, and which MCP tools the key may reach all hang off it. That single point of control is the design's real argument.&lt;/p&gt;

&lt;p&gt;Observability is built in rather than bolted on. Inputs, outputs, model, provider, token counts, cost, latency, and errors are recorded automatically, stored in SQLite or PostgreSQL, and exportable to &lt;a href="https://docs.getbifrost.ai/features/observability/otel" rel="noopener noreferrer"&gt;OTel&lt;/a&gt;, &lt;a href="https://docs.getbifrost.ai/features/observability/prometheus" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt;, Datadog, Kafka, Splunk, BigQuery, and Pub/Sub. &lt;a href="https://docs.getbifrost.ai/features/observability/content-logging" rel="noopener noreferrer"&gt;Content logging&lt;/a&gt; can be disabled for the internal log store, per request, and separately for each observability connector, so metadata such as cost and latency can be retained without exporting prompt bodies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.getbifrost.ai/features/fallbacks" rel="noopener noreferrer"&gt;Fallbacks&lt;/a&gt; are per-request. I pointed a provider at a dead address and the request completed through the fallback, with the substitute model named in the response.&lt;/p&gt;

&lt;p&gt;It is also an &lt;a href="https://docs.getbifrost.ai/mcp/overview" rel="noopener noreferrer"&gt;MCP gateway&lt;/a&gt;: MCP servers and tools are centrally managed, and each virtual key can be restricted to a subset of tools. For agent workloads that is the same governance model extended one layer up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations.&lt;/strong&gt; Provider coverage is roughly a quarter of LiteLLM's, and the community is the smallest here — fewer existing answers when something breaks. Clustering, RBAC, OIDC provisioning, audit logs, and guardrails are &lt;a href="https://docs.getbifrost.ai/enterprise/overview" rel="noopener noreferrer"&gt;Enterprise&lt;/a&gt;, as are vendor-supported private-network, on-premise, and air-gapped deployments — the OSS container itself runs anywhere you can run a container. The zero-config SQLite is also a single-instance story: the &lt;a href="https://docs.getbifrost.ai/enterprise/moving-from-oss/overview" rel="noopener noreferrer"&gt;migration docs&lt;/a&gt; call it "single-writer and single-host by design, which is incompatible with HA clustering, cross-region replicas, and the connection-pool patterns Enterprise relies on," so scaling out means moving the config and log stores to PostgreSQL. &lt;a href="https://docs.getbifrost.ai/features/semantic-caching" rel="noopener noreferrer"&gt;Semantic caching&lt;/a&gt; needs a vector store attached before it does anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted: LiteLLM
&lt;/h2&gt;

&lt;p&gt;Python SDK plus proxy, &lt;strong&gt;100+ providers&lt;/strong&gt; — the widest coverage here — and the larger open-source project by a wide margin at 57.6k GitHub stars against Bifrost's 7.7k.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out: the widest provider catalogue by a large margin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenAI, Anthropic, Azure OpenAI, Bedrock, Vertex AI, Cohere, Mistral, Groq, Ollama, vLLM — cloud and self-hosted, across chat, embedding, image, audio, and rerank endpoints. If you evaluate new models as they land, or run your own inference alongside hosted APIs, this is the catalogue most likely to already cover it.&lt;/p&gt;

&lt;p&gt;Routing and multi-tenancy are correspondingly flexible: virtual key auth, user/team/organization hierarchy, budgets at key, user and team level, RPM and TPM limits, model groups, load balancing across deployments, retries, in-model failover, cross-model fallback, A/B testing, and traffic mirroring.&lt;/p&gt;

&lt;p&gt;The free tier is not a teaser. Per the &lt;a href="https://www.litellm.ai/oss" rel="noopener noreferrer"&gt;OSS page&lt;/a&gt; and &lt;a href="https://docs.litellm.ai/docs/enterprise" rel="noopener noreferrer"&gt;enterprise docs&lt;/a&gt;: virtual keys, spend tracking, budgets, rate limits, global guardrails, request/response logging, fallbacks, load balancing, and &lt;strong&gt;Prometheus metrics&lt;/strong&gt;. That last one is worth pinning down, since third-party comparisons list it as Enterprise. It isn't — enabling the callback on the OSS container (&lt;code&gt;main-latest&lt;/code&gt;, pulled 30 Aug 2026) exposed around 150 &lt;code&gt;litellm_*&lt;/code&gt; series covering request counts, latency histograms, and spend, labelled by model, team, and key, with no license gate:&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;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prometheus"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# OSS; served at /metrics/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already run an observability stack, this is the gateway that fits it: Langfuse, OpenTelemetry, LangSmith, Datadog, Arize, MLflow, S3, GCS, Azure Blob, Azure Sentinel, and custom callbacks are all supported destinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations.&lt;/strong&gt; Production topology is the real cost. Virtual keys need PostgreSQL (the &lt;a href="https://docs.litellm.ai/docs/proxy/virtual_keys" rel="noopener noreferrer"&gt;docs&lt;/a&gt; are specific it is not SQLite), Redis is required "as soon as you run more than one proxy instance," and past "roughly 1000+ requests per second" the &lt;a href="https://docs.litellm.ai/docs/proxy/prod" rel="noopener noreferrer"&gt;production guide&lt;/a&gt; recommends a Redis transaction buffer. That's sound engineering — contended budget counters across replicas need a shared transactional store — but it is more to stand up than Bifrost asks for, and the guide is required reading rather than optional.&lt;/p&gt;

&lt;p&gt;SSO is free for up to five users. Larger SSO deployments, SCIM, audit logs, secret-manager integrations, key rotation, and advanced RBAC require Enterprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managed: Cloudflare AI Gateway
&lt;/h2&gt;

&lt;p&gt;23+ providers, with analytics, logging, caching, rate limiting, retries, dynamic routing, guardrails, and DLP. Nothing to self-host, so everything here is from Cloudflare's docs rather than something I ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out: security controls that live in the gateway.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/ai-gateway/features/dlp/" rel="noopener noreferrer"&gt;DLP&lt;/a&gt; applies to prompts and responses passing through, with built-in profiles for financial and identifier data and custom profiles available under Cloudflare One. Guardrails detect harmful content and can flag or block it. If your reason for wanting a gateway is that prompts might contain data that should not reach an upstream model provider uninspected, this is the one that treats that as a first-class problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/" rel="noopener noreferrer"&gt;Dynamic Routing&lt;/a&gt; is configured in a visual editor or as JSON: conditional branches on request body, headers, or metadata; per-user, per-project, per-team rate limits; budget limits; model fallback; retries and timeouts; A/B tests; percentage rollouts; route versioning; and instant rollback. Changing which model serves traffic doesn't require an application deploy — and someone who doesn't write code can read the routing graph.&lt;/p&gt;

&lt;p&gt;Spend limits put dollar budgets in the request path, and Dynamic Routing can step down to a cheaper model when the primary model’s budget is exhausted. LiteLLM and Bifrost also support budget-triggered fallback paths; Cloudflare’s distinction is that the policy is expressed directly in the same visual routing graph.&lt;/p&gt;

&lt;p&gt;Telemetry isn't trapped in the dashboard. The &lt;a href="https://developers.cloudflare.com/ai-gateway/observability/otel-integration/" rel="noopener noreferrer"&gt;OpenTelemetry integration&lt;/a&gt; exports spans in OTLP JSON or protobuf to any compatible backend, carrying model, provider, token counts, prompts, completions, cost estimates, and custom metadata. A GraphQL analytics API and Workers Logpush cover raw logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations.&lt;/strong&gt; Two are worth knowing before you commit. Tokens are account-scoped: per the &lt;a href="https://developers.cloudflare.com/ai-gateway/configuration/authentication/" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;, &lt;code&gt;AI Gateway Run&lt;/code&gt; permission "cannot be restricted to a single gateway," so isolating tenants means separate accounts or Worker-side bindings. And &lt;a href="https://developers.cloudflare.com/ai-gateway/features/dlp/" rel="noopener noreferrer"&gt;DLP on streaming responses removes the streaming&lt;/a&gt; — it "buffers the full streamed response before scanning," so time-to-first-token grows to the full generation time. Request-only scanning avoids that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managed: Vercel AI Gateway
&lt;/h2&gt;

&lt;p&gt;Hundreds of models across many providers behind one endpoint, with BYOK on the paid tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out: the shortest distance from idea to working feature.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Integration with the Vercel AI SDK is seamless — &lt;code&gt;provider/model&lt;/code&gt; strings, one endpoint, many models — and it also speaks OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages, so it isn't Next.js-only. Modality coverage is the broadest here: text, image, video, speech, speech-to-text, and realtime voice through one gateway, which makes it closer to a multimodal API hub than an LLM proxy.&lt;/p&gt;

&lt;p&gt;Provider routing is unusually granular. By default the gateway "dynamically chooses the default providers based on a combination of recent uptime and latency," and you can override with &lt;a href="https://vercel.com/docs/ai-gateway/models-and-providers/provider-options" rel="noopener noreferrer"&gt;provider options&lt;/a&gt;: &lt;code&gt;order&lt;/code&gt; to set preference, &lt;code&gt;only&lt;/code&gt; to restrict, and &lt;code&gt;sort&lt;/code&gt; by &lt;code&gt;cost&lt;/code&gt;, &lt;code&gt;ttft&lt;/code&gt;, or &lt;code&gt;tps&lt;/code&gt;. Per-provider timeouts for BYOK credentials trigger fast failover; model fallbacks handle the case where the entire model is unavailable. Controlling &lt;em&gt;which provider serves a given model&lt;/em&gt; is something the others expose far less directly.&lt;/p&gt;

&lt;p&gt;Telemetry has a property none of the others match by default: &lt;a href="https://vercel.com/docs/ai-gateway/observability-and-spend/trace-drains" rel="noopener noreferrer"&gt;trace drains&lt;/a&gt; emit an OpenTelemetry trace per request, and "traces contain request metadata but not prompt or completion content." The others can be configured to exclude prompt bodies; here it is not configurable, which is a shorter conversation with a compliance reviewer.&lt;/p&gt;

&lt;p&gt;Budgets can be set at team, project, API-key, and team-member level. Member budgets are enforced through spend attribution: API keys assigned to a user count against both that member’s budget and the team budget. Zero Data Retention routing restricts traffic to providers with ZDR agreements. &lt;a href="https://vercel.com/docs/drains/reference/audit-logs" rel="noopener noreferrer"&gt;Audit Log Drains&lt;/a&gt; forward team activity to S3, Splunk, Datadog, or Panther.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations.&lt;/strong&gt; Tokens carry no markup, but several governance and observability features are metered separately, per the &lt;a href="https://vercel.com/docs/ai-gateway/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;. Team-wide provider allowlisting and team-wide ZDR each cost $0.10 per 1,000 requests. Custom reporting costs $0.075 per 1,000 tag, user-ID, or quota-entity writes, plus $5 per 1,000 reporting queries. Trace drains cost $0.05 per 1,000 traces plus egress. Per-request provider filtering and per-request ZDR do not carry these team-wide surcharges.&lt;/p&gt;

&lt;p&gt;DLP and content guardrails aren't the focus they are at Cloudflare; PII masking and prompt injection detection stay in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost structure
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;How you pay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bifrost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OSS free; you pay for infrastructure. Enterprise quoted individually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LiteLLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OSS free; you pay for infrastructure. Enterprise quoted on annual request capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Analytics, caching, rate limiting free. Unified Billing adds 5% on credit purchases; BYOK avoids it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provider list price, no token markup. Governance add-ons metered per request; BYOK needs paid tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The OSS licenses are free, but production deployments can still add infrastructure and operational costs: PostgreSQL, Redis, vector storage, backups, monitoring, and the people to run them, depending on the topology and features you enable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bifrost&lt;/strong&gt; — you want to self-host, want observability and governance included rather than assembled, need virtual keys tying together budgets, routing and model access, and want an MCP gateway in the same product. Fewer moving parts to stand up than LiteLLM: no Postgres or Redis until you run replicas. Start with the &lt;a href="https://docs.getbifrost.ai/quickstart/gateway/setting-up" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;; the &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;product page&lt;/a&gt; covers where the Enterprise line falls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LiteLLM&lt;/strong&gt; — provider coverage is the deciding factor, you use niche or self-hosted models, you already run Langfuse/Datadog/OpenTelemetry and want the gateway to feed them, or you need complex routing and multi-tenancy. Assumes you have a platform team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt; — you already use Cloudflare Workers or Cloudflare One, you don't want to manage gateway infrastructure, DLP and content guardrails matter, or you want visual routing with A/B tests and staged rollouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt; — you're on Next.js or the Vercel AI SDK, you want the fastest path to a working feature, you need image/video/voice alongside text, or you want automatic provider selection with a clear dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom lines
&lt;/h2&gt;

&lt;p&gt;Pick the family first — self-hosted or managed — because that decision constrains everything downstream. Then pick on the constraint that actually binds you: provider coverage, data residency, DLP, or developer velocity.&lt;/p&gt;

&lt;p&gt;The alternative to picking one is the arrangement at the top of this post: provider keys copied across services, and spend that only becomes visible when the invoice lands.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this post was made in collaboration with the Bifrost team; the testing, the comparisons, and the conclusions are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>devops</category>
      <category>observability</category>
    </item>
    <item>
      <title>Stop asking your AI agent to follow rules. Enforce them.</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:25:37 +0000</pubDate>
      <link>https://dev.to/toffy/stop-asking-your-ai-agent-to-follow-rules-enforce-them-4mlo</link>
      <guid>https://dev.to/toffy/stop-asking-your-ai-agent-to-follow-rules-enforce-them-4mlo</guid>
      <description>&lt;p&gt;You've written it a hundred times. In your &lt;code&gt;CLAUDE.md&lt;/code&gt;, in your system prompt, in ALL CAPS:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NEVER put &lt;code&gt;"use client"&lt;/code&gt; at the page level. NEVER commit &lt;code&gt;@ts-ignore&lt;/code&gt; without a reason.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And your agent does it anyway. Not always — that would almost be easier to deal with. It follows the rule for the first 50k tokens, then quietly stops. Or Sonnet follows it and Haiku doesn't. Or it follows nine rules and forgets the tenth.&lt;/p&gt;

&lt;p&gt;Here's the thing I finally accepted: &lt;strong&gt;a rule in a prompt is a request. The model can decline it.&lt;/strong&gt; So I stopped asking, and started enforcing.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prompt adherence is probabilistic. It degrades with context length and with model size.&lt;/li&gt;
&lt;li&gt;But half of my coding rules never needed a model at all — they're grep-able.&lt;/li&gt;
&lt;li&gt;Claude Code hooks + &lt;code&gt;exit 2&lt;/code&gt; turn those rules into a deterministic reviewer that runs after &lt;strong&gt;every single edit&lt;/strong&gt;, costs &lt;strong&gt;zero tokens when nothing is wrong&lt;/strong&gt;, and fires at 100% regardless of which model wrote the code.&lt;/li&gt;
&lt;li&gt;Once the mechanical rules are enforced from below, you can safely downgrade the model doing the typing. That's the real payoff.&lt;/li&gt;
&lt;li&gt;Everything below ships in &lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;ccteams v0.3.0&lt;/a&gt;, but the pattern takes 30 minutes to build yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two kinds of rules
&lt;/h2&gt;

&lt;p&gt;Some background in three lines: I run Claude Code with orchestrated agent teams — a builder writes code, a reviewer verifies it, and both get a stack-specific "playbook" of rules distilled from the mistakes mid-tier models actually make. It works well. I wrote about the prompt-engineering side of it before.&lt;/p&gt;

&lt;p&gt;But rereading my playbooks, I noticed the rules split cleanly into two categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules that need judgment:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trace the Server/Client boundary by hand.&lt;br&gt;
Don't write a fix until you can state the root cause.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These need a model. Prompts are the right place for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules that are just string matching:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;"use client"&lt;/code&gt; at the top of &lt;code&gt;app/**/page.tsx&lt;/code&gt; → wrong.&lt;br&gt;
&lt;code&gt;process.env.SECRET&lt;/code&gt; in a client file → wrong.&lt;br&gt;
&lt;code&gt;@ts-ignore&lt;/code&gt; with no justification → wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why was I asking a &lt;em&gt;language model&lt;/em&gt; to remember these? A regex doesn't get tired at 200k tokens. A regex doesn't perform worse on a smaller model. I was running deterministic checks on the most expensive, least reliable runtime available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism: exit code 2
&lt;/h2&gt;

&lt;p&gt;Claude Code has &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt; — commands that run on lifecycle events. The one you want is &lt;strong&gt;PostToolUse&lt;/strong&gt;: it fires every time the agent runs &lt;code&gt;Edit&lt;/code&gt; or &lt;code&gt;Write&lt;/code&gt;, and it receives a JSON payload on stdin telling you which file was touched.&lt;/p&gt;

&lt;p&gt;The magic is in the exit-code contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;exit 0&lt;/strong&gt; → nothing happens, the agent keeps working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exit 2&lt;/strong&gt; → whatever your script wrote to &lt;strong&gt;stderr gets injected back into the agent's context&lt;/strong&gt; as feedback on that tool call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No human in the loop. No approval dialog. The agent edits a file, and — from its point of view — the edit "responds" with a code review. It reads the feedback and fixes the problem &lt;em&gt;in the same turn&lt;/em&gt;, before a human or a reviewer agent ever sees the mistake.&lt;/p&gt;

&lt;p&gt;A minimal version is embarrassingly simple:&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="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="c1"&gt;// .claude/hooks/check.mjs — wired to PostToolUse in .claude/settings.json&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readStdin&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;file&lt;/span&gt; &lt;span class="o"&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;tool_input&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;file_path&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;file&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.tsx&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;exit&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&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="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\s&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="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;use client&lt;/span&gt;&lt;span class="se"&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;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;app&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;.*page&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;tsx$/&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;file&lt;/span&gt;&lt;span class="p"&gt;))&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;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;route-level &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: this makes the whole page render client-side. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Push it down to the smallest interactive leaf component.&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← this line is the entire trick&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a code reviewer that never sleeps, never gets context-drunk, and works for free.&lt;/p&gt;

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

&lt;p&gt;In &lt;a href="https://github.com/toffyui/ccteams" rel="noopener noreferrer"&gt;ccteams&lt;/a&gt; v0.3.0, every stack-specific team now bundles a check script built from its playbook's known failure patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Checks (excerpt)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next-ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;route-level &lt;code&gt;"use client"&lt;/code&gt;, non-&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; env in client files, &lt;code&gt;useEffect&lt;/code&gt;+&lt;code&gt;fetch&lt;/code&gt; for initial data, &lt;code&gt;fetch()&lt;/code&gt; without explicit cache intent, &lt;code&gt;@ts-ignore&lt;/code&gt;/&lt;code&gt;as any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http.Error&lt;/code&gt; not followed by &lt;code&gt;return&lt;/code&gt;, wrapping errors with &lt;code&gt;%v&lt;/code&gt; instead of &lt;code&gt;%w&lt;/code&gt;, errors discarded with &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;context.Background()&lt;/code&gt; mid-request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-fastapi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bare &lt;code&gt;except:&lt;/code&gt;, Pydantic v1 API, mutable default args, &lt;code&gt;time.sleep&lt;/code&gt;/&lt;code&gt;requests.*&lt;/code&gt; inside async code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rails&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQL interpolation in &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;update_column&lt;/code&gt;/&lt;code&gt;save(validate: false)&lt;/code&gt;, &lt;code&gt;default_scope&lt;/code&gt;, params mass-assignment, &lt;code&gt;Time.now&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;django&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;naive &lt;code&gt;datetime.now()&lt;/code&gt;, &lt;code&gt;fields = '__all__'&lt;/code&gt;, injection-prone &lt;code&gt;.raw()&lt;/code&gt;/&lt;code&gt;.extra()&lt;/code&gt;, &lt;code&gt;post_save&lt;/code&gt; signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-native&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.map&lt;/code&gt; inside &lt;code&gt;ScrollView&lt;/code&gt;, index as key, DOM APIs like &lt;code&gt;localStorage&lt;/code&gt;, unconditional &lt;code&gt;behavior="padding"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;onClick&lt;/code&gt; on a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; without &lt;code&gt;alt&lt;/code&gt;, &lt;code&gt;outline: none&lt;/code&gt; with no &lt;code&gt;:focus-visible&lt;/code&gt;, z-index escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When an agent writes a violation, it gets this back instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ccteams next-ts check — app/dashboard/page.tsx:
  - route-level "use client": this page and its entire import tree now render
    client-side. Push "use client" down to the smallest interactive leaf component.
  - client file reads process.env.API_SECRET: non-NEXT_PUBLIC_ env vars are
    undefined in the browser (or a secret leak if inlined).
Fix these now, or state in your report why each is intentional.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the escape hatch in the last line. These are nudges, not walls — the edit already happened, and sometimes &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;fetch&lt;/code&gt; is legitimate. The agent can push back with a reason, and the reviewer checks that it did one or the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats prompt rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. It fires at 100%.&lt;/strong&gt; A prompt rule needs to be read, retained, and recalled at the right moment. A hook is a grep. Token 500k? Fires. Haiku wrote the code? Fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You pay per violation, not per instruction.&lt;/strong&gt; Prompt rules cost tokens on every delegation &lt;em&gt;even when they're followed&lt;/em&gt;. A hook costs zero tokens until something is actually wrong — then it costs three lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Mistakes die before the review round-trip.&lt;/strong&gt; Builder writes bug → reviewer catches it → sends it back → builder fixes it: that loop is the expensive part of multi-agent setups. Hooks kill the mistake at write time, so your reviewer spends its (expensive) tokens on things that actually need judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: you can downgrade the model
&lt;/h2&gt;

&lt;p&gt;This is the part I didn't expect to matter so much.&lt;/p&gt;

&lt;p&gt;ccteams agents ship in two tiers — builders on Sonnet, reviewers on Opus. v0.3.0 adds model profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ccteams use next-ts &lt;span class="nt"&gt;--profile&lt;/span&gt; budget    &lt;span class="c"&gt;# builder: haiku / reviewer: sonnet&lt;/span&gt;
ccteams use next-ts                     &lt;span class="c"&gt;# builder: sonnet / reviewer: opus&lt;/span&gt;
ccteams use next-ts &lt;span class="nt"&gt;--profile&lt;/span&gt; max       &lt;span class="c"&gt;# everyone: opus&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting Haiku to &lt;em&gt;remember&lt;/em&gt; a playbook through prompts alone is a losing game. But hooks fire on Haiku's code with exactly the same precision as on Opus's. With deterministic checks backing it from below, &lt;code&gt;--profile budget&lt;/code&gt; stops being "cheap and broken" and becomes a legitimate configuration for routine work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The more discipline you mechanize, the less intelligence you need to rent.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary (don't hook everything)
&lt;/h2&gt;

&lt;p&gt;Hooks only cover rules that are mechanically checkable without false positives. "Trace the data flow," "run the actual build and quote its output," "state the root cause before fixing" — those stay in prompts and reviewer gates, and I have no intention of moving them.&lt;/p&gt;

&lt;p&gt;The rule of thumb I've landed on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If a violation can be detected with grep, enforce it with a hook. If it needs judgment, put it in the prompt.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One design note if you build this yourself: make your hook scripts fail silent (&lt;code&gt;catch → exit 0&lt;/code&gt;). A buggy check that crashes loudly will poison every editing session. And namespace your hook entries (ours all live at &lt;code&gt;.claude/hooks/ccteams-*&lt;/code&gt;) so that installing/removing them never touches hooks the user wrote themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&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; &lt;span class="nt"&gt;-g&lt;/span&gt; ccteams
ccteams use next-ts &lt;span class="nt"&gt;--profile&lt;/span&gt; budget   &lt;span class="c"&gt;# or go-api, rails, django, python-fastapi...&lt;/span&gt;
&lt;span class="c"&gt;# restart Claude Code — hooks and agents load at session start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command gets you the agent team, the playbook, the hooks, and the cost profile. Switching teams swaps the hooks cleanly.&lt;/p&gt;

&lt;p&gt;If this saves you a round-trip or two, a star on the &lt;a href="https://github.com/toffyui/ccteams" rel="noopener noreferrer"&gt;repo&lt;/a&gt; genuinely helps. And I'd love to hear what checks you'd add for your stack — the whole point of this design is that a new rule is one regex away.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Apple quietly shipped everything you need to build a real-time translator — so I built one</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Tue, 11 Aug 2026 20:25:27 +0000</pubDate>
      <link>https://dev.to/toffy/apple-quietly-shipped-everything-you-need-to-build-a-real-time-translator-so-i-built-one-9ce</link>
      <guid>https://dev.to/toffy/apple-quietly-shipped-everything-you-need-to-build-a-real-time-translator-so-i-built-one-9ce</guid>
      <description>&lt;p&gt;I work at a German company. Meetings are in German, sometimes English — and even though I speak both, there are moments in fast meetings where I zone out for two seconds and think: &lt;em&gt;wait, what did they just say?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I couldn't use any of the existing captioning tools, because they all pipe your meeting audio to a cloud server. Sending confidential work calls to a third party was a non-starter.&lt;/p&gt;

&lt;p&gt;Then I realized something: &lt;strong&gt;macOS 26 quietly shipped every building block you need for a real-time translator.&lt;/strong&gt; On-device speech recognition (SpeechAnalyzer), on-device machine translation (Translation framework), and an on-device LLM (FoundationModels / Apple Intelligence). No servers, no API keys, no per-minute fees.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://apps.apple.com/us/app/wakaru-live-caption-translate/id6796070776" rel="noopener noreferrer"&gt;&lt;strong&gt;Wakaru&lt;/strong&gt;&lt;/a&gt; — a menu bar app that turns &lt;em&gt;any&lt;/em&gt; audio playing on your Mac into movie-style translated subtitles, in real time.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎬 Live translated subtitles for &lt;strong&gt;anything your Mac plays&lt;/strong&gt; — Teams, Zoom, YouTube, webinars, podcasts&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;100% on-device.&lt;/strong&gt; Your audio never leaves your Mac. No account, no API keys, no cloud&lt;/li&gt;
&lt;li&gt;🌍 Recognizes 10 spoken languages, subtitles in 22&lt;/li&gt;
&lt;li&gt;🧠 Optional high-accuracy mode using the on-device Apple Intelligence LLM&lt;/li&gt;
&lt;li&gt;⚡ Built entirely with Apple's native frameworks — zero external dependencies, ~3,000 lines of Swift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers how it works and the four gotchas that cost me the most time. If you're planning to build anything on macOS 26's new speech or translation APIs, this might save you a few days.&lt;/p&gt;

&lt;h2&gt;
  
  
  First attempt: Electron (and why I threw it away)
&lt;/h2&gt;

&lt;p&gt;My first prototype was Electron. It worked — but transcription + translation took about &lt;strong&gt;2 seconds per sentence&lt;/strong&gt;. For subtitles, 2 seconds might not sound like a lot, but it is: by the time the caption showed up, the conversation had moved on.&lt;/p&gt;

&lt;p&gt;When I saw that macOS 26 had the entire pipeline available natively and on-device, I rebuilt it in Swift. The difference was dramatic — captions now appear while the sentence is still being spoken.&lt;/p&gt;

&lt;p&gt;(The Electron version wasn't wasted, though. After a lot of tuning it got reasonably fast, and I'm planning to release it for Windows soon.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: four stages, zero dependencies
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ScreenCaptureKit ──▶ SpeechAnalyzer ──▶ Translation framework ──▶ Subtitle overlay
 (system audio)      (speech-to-text)    or FoundationModels        (NSPanel)
                                          (translation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key decision: capture &lt;strong&gt;system audio&lt;/strong&gt; (what the Mac is playing) instead of the microphone. That's what makes Wakaru app-agnostic — it doesn't integrate with Zoom or Teams; it doesn't need to know they exist.&lt;/p&gt;

&lt;p&gt;The subtitles are drawn on a borderless, transparent, click-through &lt;code&gt;NSPanel&lt;/code&gt;, so you can click straight through the captions to whatever is underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it feel like movie subtitles (not a log viewer)
&lt;/h2&gt;

&lt;p&gt;The naive version of this app is a window where original text and translations pile up like a chat log. That's easy to build — and unusable as subtitles.&lt;/p&gt;

&lt;p&gt;What I ended up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't translate while a sentence is still forming.&lt;/strong&gt; I initially translated the live transcription in real time. For a verb-final language like German, the translation reshuffled itself on every update — completely unreadable. Now Wakaru waits until a sentence (or a long clause) is complete, then translates it exactly once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caption lifetime scales with reading speed.&lt;/strong&gt; Each caption stays on screen for &lt;code&gt;text.count / 7 + 2&lt;/code&gt; seconds (clamped to 4–12s). Short interjections vanish quickly; long sentences stay until you can actually finish them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every caption gets a minimum of 2.5 seconds&lt;/strong&gt;, even when someone is talking fast — new captions queue up instead of instantly evicting the old ones.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lifetime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotcha #1: Set the sample rate at capture time
&lt;/h2&gt;

&lt;p&gt;SpeechAnalyzer prefers 16 kHz audio. My first version captured at 48 kHz and resampled with &lt;code&gt;AVAudioConverter&lt;/code&gt; — and the first caption took &lt;em&gt;seconds&lt;/em&gt; to appear. The converter buffers audio internally before it emits anything.&lt;/p&gt;

&lt;p&gt;The fix: ScreenCaptureKit lets you pick the sample rate at capture time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SCStreamConfiguration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capturesAudio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16_000&lt;/span&gt;   &lt;span class="c1"&gt;// match SpeechAnalyzer's preferred format&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channelCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Capture at 16 kHz mono from the start and the only conversion left is Float32 → Int16, sample by sample. The latency disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: SpeechAnalyzer is slow by default — in three different ways
&lt;/h2&gt;

&lt;p&gt;Even after fixing the sample rate, captions were still sluggish. It turned out to be three &lt;em&gt;separate&lt;/em&gt; problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reporting cadence.&lt;/strong&gt; By default, SpeechAnalyzer batches up partial results and delivers them in bursts, seconds late. Pass &lt;code&gt;.fastResults&lt;/code&gt; in &lt;code&gt;reportingOptions&lt;/code&gt; to get them as they happen. For live captions this is non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy model loading.&lt;/strong&gt; The recognition model is big, and by default it loads when the &lt;em&gt;first audio arrives&lt;/em&gt; — so your first caption is seconds late. Call &lt;code&gt;prepareToAnalyze&lt;/code&gt; when the user hits start, and recognition is instant from the first word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow shutdown.&lt;/strong&gt; Stopping with &lt;code&gt;finalizeAndFinishThroughEndOfInput()&lt;/code&gt; drains the entire audio backlog before returning — a stop/restart (e.g. switching languages) took seconds. For subtitles you don't care about queued audio, so use &lt;code&gt;cancelAndFinishNow()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Gotcha #3: Recognition results rewrite the past
&lt;/h2&gt;

&lt;p&gt;This was the biggest trap of all.&lt;/p&gt;

&lt;p&gt;SpeechAnalyzer's partial results don't just grow at the end. &lt;strong&gt;Text you already displayed gets rewritten retroactively&lt;/strong&gt; — filler words ("uh, uh") get collapsed, words get swapped, punctuation appears late.&lt;/p&gt;

&lt;p&gt;Wakaru cuts completed sentences out of the growing transcript and translates each one. That means it has to remember &lt;em&gt;where the already-translated part ends&lt;/em&gt;. If you store that boundary as a character offset, it silently drifts every time the recognizer rewrites history. The symptoms: the same sentence gets translated twice, or fragments go missing.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;stop trusting positions, and use content as a bookmark.&lt;/strong&gt; Wakaru remembers the last few words of the most recently committed sentence, and on every update, &lt;em&gt;searches for that anchor&lt;/em&gt; in the rewritten text to re-derive the boundary.&lt;/p&gt;

&lt;p&gt;One wrinkle remains: if the speaker literally says the same thing twice ("Thank you. Thank you."), the anchor appears in two places. So the rule is "pick the occurrence &lt;em&gt;closest to the previous boundary estimate&lt;/em&gt;" — the character offset survives, demoted from source-of-truth to tie-breaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: The Translation framework only works inside SwiftUI
&lt;/h2&gt;

&lt;p&gt;This one surprised me the most. &lt;code&gt;TranslationSession&lt;/code&gt; can only be obtained inside SwiftUI's &lt;code&gt;.translationTask&lt;/code&gt; view modifier. There is no "just give me a session" API. Wakaru is a menu bar app — there was nowhere natural to put it.&lt;/p&gt;

&lt;p&gt;The workaround: the subtitle overlay is a SwiftUI view anyway, so I attached an invisible &lt;code&gt;.translationTask&lt;/code&gt; to it. It receives the session and hands it to a hub object that the rest of the app calls into.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translationTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// publish the session to the pipeline&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bonus trap: if you stop and restart with the &lt;em&gt;same&lt;/em&gt; language pair, the new configuration compares equal to the old one and the task never restarts — you have to call &lt;code&gt;invalidate()&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: taming the on-device LLM
&lt;/h2&gt;

&lt;p&gt;On Apple Intelligence Macs, Wakaru has a high-accuracy mode that translates with the on-device LLM instead of the NMT model, passing the previous 3 sentences as context — pronouns, idioms, and short replies come out much more natural.&lt;/p&gt;

&lt;p&gt;But a ~3B on-device model in a real-time loop needs guardrails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2.5s deadline&lt;/strong&gt; — if the LLM is slower, that sentence silently falls back to the standard engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild the session every 10 sentences&lt;/strong&gt; — as the session transcript grows, the model starts echoing source-language fragments into translations (measured: around sentence 27)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarantine degenerate output&lt;/strong&gt; — greedy decoding occasionally locks into repetition loops (a normal German sentence once became the same Japanese word ×25). There's no repetition penalty setting, so I detect and reject it at the output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that matters: &lt;strong&gt;the LLM is never allowed to stall the captions.&lt;/strong&gt; Every failure mode falls back per-sentence to the standard engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / tell me what you think
&lt;/h2&gt;

&lt;p&gt;Wakaru runs on macOS 26+ (Apple silicon). It's free for 14 days (and stays free for 1 hour/day after that), with a one-time purchase for unlimited time — no subscription:&lt;/p&gt;

&lt;p&gt;📱 &lt;a href="https://apps.apple.com/us/app/wakaru-live-caption-translate/id6796070776" rel="noopener noreferrer"&gt;Wakaru on the Mac App Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer any questions about the implementation in the comments! 👇&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>swift</category>
      <category>ai</category>
      <category>macos</category>
    </item>
    <item>
      <title>How I export 1.2-gigapixel images on an iPhone without running out of memory</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Mon, 13 Jul 2026 21:38:34 +0000</pubDate>
      <link>https://dev.to/toffy/how-i-export-12-gigapixel-images-on-an-iphone-without-running-out-of-memory-1hkk</link>
      <guid>https://dev.to/toffy/how-i-export-12-gigapixel-images-on-an-iphone-without-running-out-of-memory-1hkk</guid>
      <description>&lt;p&gt;Rendering a big image on iOS is one of those things that looks trivial until your app gets killed by the OS mid-export. &lt;code&gt;CGContext&lt;/code&gt;, draw, &lt;code&gt;makeImage()&lt;/code&gt;, done — except the moment the output gets large, that innocent-looking pipeline quietly asks for &lt;strong&gt;gigabytes&lt;/strong&gt; of RAM and iOS terminates you.&lt;/p&gt;

&lt;p&gt;I hit this wall building &lt;a href="https://apps.apple.com/us/app/mozary/id6759013945" rel="noopener noreferrer"&gt;Mozary&lt;/a&gt;, an iOS app that packs 100+ photos into a single giant picture (a photo mosaic). In v1.1.0 I finally killed the "high-resolution export crashes with out-of-memory" bug for good.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;stop putting the canvas in RAM at all.&lt;/strong&gt; Put it in a memory-mapped file and let the OS page it to disk. RAM usage dropped from &lt;em&gt;"4.8 GB, please die"&lt;/em&gt; to &lt;em&gt;a few dozen MB, flat, regardless of output size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post is the walkthrough — with the actual Swift. If you've ever seen Core Graphics blow up on a big image (mosaics, collages, stitched panoramas, high-res rendering — same trap), this is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A non-compressed bitmap costs &lt;strong&gt;4 bytes/pixel&lt;/strong&gt;. A 1.2-gigapixel image = &lt;strong&gt;~4.8 GB of RAM&lt;/strong&gt; just for the canvas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CGContext(data: nil, ...)&lt;/code&gt; allocates that in RAM. &lt;code&gt;context.makeImage()&lt;/code&gt; then &lt;strong&gt;copies it again&lt;/strong&gt;. Double death.&lt;/li&gt;
&lt;li&gt;Back the canvas with a &lt;strong&gt;memory-mapped file&lt;/strong&gt; (&lt;code&gt;mmap&lt;/code&gt;). Writes transparently page out to disk and &lt;strong&gt;don't count against your app's memory footprint&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Wrap that same mapping in a &lt;code&gt;CGImage&lt;/code&gt; via &lt;code&gt;CGDataProvider&lt;/code&gt; — &lt;strong&gt;zero copy&lt;/strong&gt; — and stream it straight to a JPEG on disk. Never call &lt;code&gt;makeImage()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Decode source tiles at their &lt;strong&gt;draw size&lt;/strong&gt;, not full size.&lt;/li&gt;
&lt;li&gt;Because you now spend disk instead of RAM: add a &lt;strong&gt;free-space pre-flight check&lt;/strong&gt; and &lt;strong&gt;clean up&lt;/strong&gt; temp files after a crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dig in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: "compressed file size" is a lie about memory
&lt;/h2&gt;

&lt;p&gt;Mozary lays photos out on a grid. A typical high-res export is a &lt;code&gt;200 × 267&lt;/code&gt; grid with each tile drawn at &lt;code&gt;150px&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;width:  200 × 150 = 30,000 px
height: 267 × 150 = 40,050 px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;strong&gt;~1.2 gigapixels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the part people underestimate: the final JPEG is only a few hundred MB to ~2 GB because JPEG is &lt;em&gt;compressed&lt;/em&gt;. But &lt;strong&gt;while you're drawing, the canvas is uncompressed&lt;/strong&gt; — 4 bytes per pixel (RGBA):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30,000 × 40,050 × 4 bytes ≈ 4.8 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4.8 GB of RAM for the canvas alone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;iOS caps how much memory an app may use (varies by device, but you'll get jetsammed somewhere in the few-hundred-MB-to-~2-GB range). 4.8 GB is never happening. That's the crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive way (what I had before)
&lt;/h2&gt;

&lt;p&gt;My original export looked like this — and honestly, it was brute force:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ OLD: allocate the giant bitmap in RAM&lt;/span&gt;

&lt;span class="c1"&gt;// "Pre-flight": compute how many bytes the bitmap needs&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;bitmapBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imgHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;availableMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;os_proc_available_memory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bitmapBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.3&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;availableMemory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Not enough → just give up on high-res&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MediaExporter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&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="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;// data: nil → CGContext allocates the bitmap in RAM&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bitsPerComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;colorSpace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bitmapInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGImageAlphaInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premultipliedLast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="c1"&gt;// ... draw all the tiles ...&lt;/span&gt;

&lt;span class="c1"&gt;// makeImage() duplicates the whole bitmap (another +4.8 GB)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cgImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeImage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CGContext(data: nil, ...)&lt;/code&gt; allocates the canvas in RAM&lt;/strong&gt; → asks for 4.8 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.makeImage()&lt;/code&gt; copies the canvas again&lt;/strong&gt; → +4.8 GB at peak.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;os_proc_available_memory()&lt;/code&gt; check wasn't a fix — it was a polite &lt;em&gt;"sorry, give up."&lt;/em&gt; High-res export only worked on the beefiest devices, and mid-tier sizes could sneak past the check and then crash on the &lt;code&gt;makeImage()&lt;/code&gt; copy anyway.&lt;/p&gt;

&lt;p&gt;I needed to render the same pixels &lt;strong&gt;without ever holding them all in RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: put the canvas on disk, not in RAM
&lt;/h2&gt;

&lt;p&gt;The reframe that unlocked everything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If 4.8 GB won't fit in RAM, don't put it in RAM.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enter the &lt;strong&gt;memory-mapped file&lt;/strong&gt; (&lt;code&gt;mmap&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;mmap&lt;/code&gt; actually buys you
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt; maps a file into your address space. You get a pointer you read and write like normal memory — but the backing store is a &lt;strong&gt;file on disk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The magic is that the OS manages that region in &lt;strong&gt;pages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages you write become "dirty" and the OS &lt;strong&gt;writes them back to disk&lt;/strong&gt; as needed.&lt;/li&gt;
&lt;li&gt;Once written back (clean), those pages &lt;strong&gt;no longer count against your app's memory footprint&lt;/strong&gt; (the thing that gets you jetsammed).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even if the canvas is 4.8 GB, the only thing resident in RAM is &lt;strong&gt;the handful of pages you're touching right now&lt;/strong&gt;. Everything else lives on disk. You never trip the memory limit.&lt;/p&gt;

&lt;p&gt;In one sentence: &lt;strong&gt;I stopped trying to fit the work in the app's own memory, and the crash disappeared.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — create the file and map it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;ppc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;effectivePixelsPerCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;imgWidth&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;imgHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;bytesPerRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imgWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;totalBytes&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;imgHeight&lt;/span&gt;   &lt;span class="c1"&gt;// ← can be ~4.8 GB&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Create a temp file&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;canvasURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temporaryDirectory&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mozary_canvas_&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;.raw"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;atPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvasURL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Open it and grow it to the needed size (ftruncate)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvasURL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;O_RDWR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;fd&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="nf"&gt;ftruncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;off_t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fd&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="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&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="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvasURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MediaExporter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&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;// 3. Map the file into memory&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;PROT_READ&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;PROT_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;MAP_SHARED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&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="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// The mapping keeps the file alive, so we can close the fd&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kt"&gt;MAP_FAILED&lt;/span&gt; &lt;span class="k"&gt;else&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="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvasURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MediaExporter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&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;// 4. ALWAYS release the mapping, on every exit path&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;munmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalBytes&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="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvasURL&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;code&gt;ftruncate&lt;/code&gt; grows the file to &lt;code&gt;totalBytes&lt;/code&gt;, but it creates a &lt;strong&gt;sparse file&lt;/strong&gt; — disk is only actually consumed for the bytes you write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — hand the mapping to &lt;code&gt;CGContext&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CGContext&lt;/code&gt; lets you pass &lt;strong&gt;your own buffer pointer&lt;/strong&gt; as &lt;code&gt;data:&lt;/code&gt;. Just give it the &lt;code&gt;mmap&lt;/code&gt; pointer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ← the mmap'd file, not RAM&lt;/span&gt;
    &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bitsPerComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGColorSpaceCreateDeviceRGB&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nv"&gt;bitmapInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGImageAlphaInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premultipliedLast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&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;// Then just... draw. The destination is transparently disk-backed;&lt;/span&gt;
&lt;span class="c1"&gt;// the Core Graphics code doesn't change at all.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkCancellation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... context.draw(tileCGImage, in: rect) ...&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 is the beautiful part: &lt;strong&gt;from Core Graphics' point of view, nothing changed.&lt;/strong&gt; It thinks it's drawing into a plain memory buffer. The writes just quietly spill to disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — nudge finished rows out to disk
&lt;/h3&gt;

&lt;p&gt;The OS will page dirty pages out on its own, but you can help it: tell it explicitly &lt;em&gt;"I'm done with this region, feel free to flush it."&lt;/em&gt; That keeps the set of dirty (not-yet-written-back) pages small. The tool is &lt;code&gt;msync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cellProgress&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Every 40 rows, async-flush the scanlines we've already finished.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="o"&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;row&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;flushedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;
        &lt;span class="nf"&gt;msync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flushedBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;MS_ASYNC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// MS_ASYNC = non-blocking&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;Why &lt;code&gt;(row * ppc) * bytesPerRow&lt;/code&gt;? A bitmap is a &lt;strong&gt;single contiguous byte array&lt;/strong&gt; in memory — rows laid out top to bottom. My draw loop goes top row → bottom row, so once I'm on row &lt;em&gt;N&lt;/em&gt;, every row above it is finished and lives at the &lt;strong&gt;start&lt;/strong&gt; of the buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;buffer: [==== finished ====][ drawing ][=== empty ===]
        ↑ canvas            ↑
        |←── flushedBytes ──→|
        msync(MS_ASYNC) on this range =
        "you can write these pages back now"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;row * ppc&lt;/code&gt; → number of finished pixel rows (&lt;code&gt;ppc&lt;/code&gt; = pixels per cell)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;× bytesPerRow&lt;/code&gt; → convert rows to bytes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One nuance worth stating precisely: &lt;code&gt;MS_ASYNC&lt;/code&gt; &lt;strong&gt;doesn't free memory directly.&lt;/strong&gt; It marks those pages clean (safely written to disk). &lt;em&gt;Clean&lt;/em&gt; pages are the ones the OS can evict for free when it needs RAM back — because an identical copy already exists on disk. So &lt;code&gt;msync&lt;/code&gt; doesn't reclaim memory; it &lt;strong&gt;makes memory reclaimable&lt;/strong&gt;, which keeps your peak footprint low.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other trap: &lt;code&gt;makeImage()&lt;/code&gt; copies everything
&lt;/h2&gt;

&lt;p&gt;Second landmine: encoding the JPEG.&lt;/p&gt;

&lt;p&gt;Remember &lt;code&gt;context.makeImage()&lt;/code&gt; &lt;strong&gt;duplicates the whole bitmap&lt;/strong&gt;. Call it here and you've just asked for +4.8 GB — undoing everything.&lt;/p&gt;

&lt;p&gt;So skip it. Wrap the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;mmap&lt;/code&gt; region in a &lt;code&gt;CGImage&lt;/code&gt; directly, handing the pointer to &lt;code&gt;CGDataProvider&lt;/code&gt; &lt;strong&gt;with no copy&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Make sure the whole canvas is synced to disk&lt;/span&gt;
&lt;span class="nf"&gt;msync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;MS_SYNC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Wrap the mapping in a CGImage WITHOUT copying it.&lt;/span&gt;
&lt;span class="c1"&gt;// The empty releaseData is intentional: the deferred munmap above&lt;/span&gt;
&lt;span class="c1"&gt;// owns the mapping, and it outlives this scope-local image.&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGDataProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;dataInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;releaseData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// no-op&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cgImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imgHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bitsPerComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bitsPerPixel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGColorSpaceCreateDeviceRGB&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nv"&gt;bitmapInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGBitmapInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGImageAlphaInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premultipliedLast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;shouldInterpolate&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="nv"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultIntent&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&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;// Write JPEG straight to a file (no UIImage in between)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tempURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temporaryDirectory&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jpg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGImageDestinationCreateWithURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tempURL&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"public.jpeg"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&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="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&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="kt"&gt;CGImageDestinationAddImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cgImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;kCGImageDestinationLossyCompressionQuality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jpegQuality&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kt"&gt;CGImageDestinationFinalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tempURL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CGImageDestinationFinalize&lt;/code&gt; reads from the &lt;code&gt;CGImage&lt;/code&gt; (i.e. the mmap'd file) while it writes the JPEG. The encoder pulls data in tiles, so no giant buffer ever lands fully in RAM here either.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;both ends of the pipeline — the canvas input and the JPEG output — stay off the RAM budget.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus win: decode tiles at draw size, not full size
&lt;/h2&gt;

&lt;p&gt;One more thing that mattered. Mozary stores each tile's thumbnail as a 600px JPEG on disk. Decode all of them at full size for a 1,000-photo mosaic and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;600 × 600 × 4 bytes × 1000 photos ≈ 1.4 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1.4 GB just for the tile cache — but I only ever draw them at cell size (&lt;code&gt;ppc&lt;/code&gt;, 60–150px). So decode them straight to that size with &lt;code&gt;CGImageSourceCreateThumbnailAtIndex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;thumbOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;kCGImageSourceCreateThumbnailFromImageAlways&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="nv"&gt;kCGImageSourceShouldCacheImmediately&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="nv"&gt;kCGImageSourceCreateThumbnailWithTransform&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="nv"&gt;kCGImageSourceThumbnailMaxPixelSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;   &lt;span class="c1"&gt;// ← decode at cell size&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;

&lt;span class="c1"&gt;// CGImageSourceCreateThumbnailAtIndex(source, 0, thumbOptions)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 100px a tile is ~40 KB decoded. 1,000 of them fit in a few dozen MB. &lt;strong&gt;Never decode at a higher resolution than you'll draw&lt;/strong&gt; — downsampling on decode is a Core Graphics rule of thumb worth tattooing somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spending disk means guarding disk
&lt;/h2&gt;

&lt;p&gt;Trading RAM for disk creates a new constraint: &lt;strong&gt;free disk space.&lt;/strong&gt; During export the uncompressed canvas (4.8 GB) and the JPEG being written (~2 GB) coexist — a peak of ~7 GB.&lt;/p&gt;

&lt;p&gt;So I check the &lt;em&gt;actual working space needed&lt;/em&gt;, not the final file size, before starting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The final "~2 GB" file size badly understates what export transiently needs.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;requiredBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requiredWorkingBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;freeSpace&lt;/span&gt; &lt;span class="o"&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="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temporaryDirectory&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volumeAvailableCapacityForImportantUsageKey&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volumeAvailableCapacityForImportantUsage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;freeSpace&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="n"&gt;freeSpace&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;requiredBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// "needs about N GB of temporary free space"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;requiredWorkingBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;ppc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;effectivePixelsPerCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ppc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// uncompressed canvas (4B/px) + JPEG being encoded + safety margin&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;estimatedBytesPerPixel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;500_000_000&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I actually found this the hard way. On a test device with ~2 GB free, the export failed even though my &lt;em&gt;original&lt;/em&gt; check (which only accounted for the final file) said there was room. Testing on a nearly-full device is what surfaced it — the real working set needs the transient space, obviously in hindsight. Glad I debugged on a cramped device.&lt;/p&gt;

&lt;p&gt;And if the app gets killed mid-export, the &lt;code&gt;defer&lt;/code&gt; cleanup never runs and a 2 GB+ temp file is orphaned — which would then eat into the next export's disk check. So I sweep old canvas files on start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sweep temp canvases orphaned by a previous crash&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;leftovers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentsOfDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;includingPropertiesForKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&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="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;leftovers&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastPathComponent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mozary_canvas_"&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="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&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;(I also clamp cell size so no dimension exceeds JPEG's 65,535px limit, plus a few other small guards.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-res export&lt;/td&gt;
&lt;td&gt;Rejected by pre-flight, or crashed on some devices&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RAM&lt;/td&gt;
&lt;td&gt;~4.8 GB (canvas) + ~4.8 GB (&lt;code&gt;makeImage&lt;/code&gt; copy)&lt;/td&gt;
&lt;td&gt;A few dozen MB, flat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depends on output size?&lt;/td&gt;
&lt;td&gt;Yes — bigger = death&lt;/td&gt;
&lt;td&gt;No — constant footprint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ordinary iPhones now export 1.2-gigapixel mosaics without breaking a sweat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On iOS, drawing a big image blows up because the &lt;strong&gt;uncompressed canvas (4 B/px)&lt;/strong&gt; exceeds the memory limit — the compressed file size tells you nothing about that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back the canvas with &lt;code&gt;mmap&lt;/code&gt;&lt;/strong&gt; so writes page out to disk and stay off your app's memory footprint.&lt;/li&gt;
&lt;li&gt;For the output &lt;code&gt;CGImage&lt;/code&gt;, &lt;strong&gt;avoid &lt;code&gt;context.makeImage()&lt;/code&gt; (it copies)&lt;/strong&gt; — wrap the mmap region with &lt;code&gt;CGDataProvider&lt;/code&gt; for a &lt;strong&gt;zero-copy&lt;/strong&gt; hand-off, then stream to disk with &lt;code&gt;CGImageDestination&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode inputs at draw resolution&lt;/strong&gt;, never larger.&lt;/li&gt;
&lt;li&gt;Once you spend disk instead of RAM, pair it with a &lt;strong&gt;free-space pre-flight&lt;/strong&gt; and &lt;strong&gt;post-crash cleanup&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general principle — &lt;em&gt;"if it won't fit in RAM, back it with a file and let the OS page it"&lt;/em&gt; — goes way beyond Core Graphics. Any time you're wrangling a buffer bigger than memory, &lt;code&gt;mmap&lt;/code&gt; is worth reaching for.&lt;/p&gt;

&lt;p&gt;If you've fought the same "Core Graphics eats all my memory" battle, I hope this saves you a few crash reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out!
&lt;/h2&gt;

&lt;p&gt;Mozary is on the App Store if you want to see the export in action (and I'd genuinely love feedback):&lt;br&gt;
👉 &lt;a href="https://apps.apple.com/us/app/mozary/id6759013945" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/mozary/id6759013945&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's your go-to trick for large-image or large-buffer work on mobile? I'd love to hear it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>performance</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Want to keep using Fable 5? Teach Opus and Sonnet to "behave" like it.</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Mon, 06 Jul 2026 21:45:11 +0000</pubDate>
      <link>https://dev.to/toffy/want-to-keep-using-fable-5-teach-opus-and-sonnet-to-behave-like-it-4kl0</link>
      <guid>https://dev.to/toffy/want-to-keep-using-fable-5-teach-opus-and-sonnet-to-behave-like-it-4kl0</guid>
      <description>&lt;p&gt;A while back I wrote about how I use Fable 5 without going token-bankrupt:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/toffy/use-fable-5-where-it-pays-for-itself-1809" class="crayons-story__hidden-navigation-link"&gt;Use Fable 5 where it pays for itself&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="/toffy" 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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636738%2Fe52866ed-1547-49e3-930c-4f54ea9467d3.jpeg" alt="toffy profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/toffy" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yuiko Koyanagi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yuiko Koyanagi
                
                
              
              &lt;div id="story-author-preview-content-4054238" 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="/toffy" 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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636738%2Fe52866ed-1547-49e3-930c-4f54ea9467d3.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yuiko Koyanagi&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/toffy/use-fable-5-where-it-pays-for-itself-1809" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 2&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/toffy/use-fable-5-where-it-pays-for-itself-1809" id="article-link-4054238"&gt;
          Use Fable 5 where it pays for itself
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/claudecode"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;claudecode&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&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/toffy/use-fable-5-where-it-pays-for-itself-1809" 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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/toffy/use-fable-5-where-it-pays-for-itself-1809#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;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;
            7 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;I put &lt;strong&gt;Fable 5 in the orchestrator seat&lt;/strong&gt; — scoping, design, delegation, final review — while the hands-on implementation, testing, shipping, and code review are done by &lt;strong&gt;Opus and Sonnet subagents&lt;/strong&gt;. The expensive model plans and judges; the cheaper models execute. This ran beautifully for most tasks.&lt;/p&gt;

&lt;p&gt;There's just one problem. As you probably know, &lt;strong&gt;Fable 5 access via subscription ends on July 7.&lt;/strong&gt; (I'm praying it comes back to subscription someday… 🙏)&lt;/p&gt;

&lt;p&gt;The orchestration setup itself works without Fable 5 — but if the model at the top, the one cutting scope and judging everything, gets downgraded, output quality drops with it.&lt;/p&gt;

&lt;p&gt;Here's the thing though: Opus and Sonnet are not bad models. Used correctly, they produce genuinely good output. What matters is writing down &lt;em&gt;what&lt;/em&gt; to do, &lt;em&gt;how&lt;/em&gt; to do it, and &lt;em&gt;what to do when something fails&lt;/em&gt;. So before losing access, I decided to document how Fable 5 actually works and hand it down to Opus/Sonnet — as skills.&lt;/p&gt;

&lt;p&gt;This post is about what I did and how. Finding a "replacement for Fable" seemed hopeless, so the goal became something else: &lt;strong&gt;raise the whole team's capability so that quality holds even with Opus in the orchestrator seat.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between Fable 5 and Opus/Sonnet is not intelligence
&lt;/h2&gt;

&lt;p&gt;To keep using a pseudo-Fable after the shutdown, I started by examining what the difference actually is.&lt;/p&gt;

&lt;p&gt;When Opus/Sonnet produce output I didn't intend during everyday engineering, the failures turn out to follow &lt;strong&gt;recognizable patterns&lt;/strong&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slapping &lt;code&gt;"use client"&lt;/code&gt; at the top of a Next.js page because one button needs an &lt;code&gt;onClick&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;…which drops the entire page and its import tree onto the client&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;fetch&lt;/code&gt; with no cache option

&lt;ul&gt;
&lt;li&gt;The defaults are &lt;em&gt;opposite&lt;/em&gt; between Next.js 14 and 15, but "it works" was only checked on the dev server — hello, production caching surprise&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Skipping input validation in a Server Action because it's "only called from my form"

&lt;ul&gt;
&lt;li&gt;It's actually a public POST endpoint anyone can hit with arbitrary arguments&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fetching initial data inside &lt;code&gt;useEffect&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;await&lt;/code&gt; in a Server Component would do it, without the request waterfall and the layout flash&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;"Fixing" a failing test by rewriting the assertion

&lt;ul&gt;
&lt;li&gt;Don't touch the assertion until you've proven, with evidence, that the code side is the wrong one&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Saying "this should work now" without running anything

&lt;ul&gt;
&lt;li&gt;Run it first, &lt;em&gt;then&lt;/em&gt; conclude whether it works&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What matters here: &lt;strong&gt;none of these are knowledge gaps.&lt;/strong&gt; Ask Sonnet directly — "is a Next.js Server Action dangerous without input validation?" — and it answers correctly. The knowledge is in there. What's missing is the &lt;strong&gt;correct procedure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fable 5 runs these checks implicitly, without being asked, and pushes back when something's off. With Opus/Sonnet, those checks have to be written down explicitly.&lt;/p&gt;

&lt;p&gt;Raw reasoning differences are real, of course. But for routine implementation, review, and debugging, most of the observable quality gap comes down to &lt;strong&gt;discipline, sequencing, and verification&lt;/strong&gt; — and all three can be written as skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Be careful" prompts don't work
&lt;/h2&gt;

&lt;p&gt;My first attempt at Fable-izing Opus/Sonnet was writing "be careful, always verify, follow best practices" into the prompt. Result: nothing happened (obviously, in hindsight). &lt;strong&gt;A sentence that doesn't change the model's next move doesn't change its behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the first rule of documenting Fable's working method was a ban list. No line may contain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"be careful" / "follow best practices" / "think hard" / "ensure quality"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every line has to be &lt;strong&gt;executable or checkable&lt;/strong&gt; — runnable as a command, defined as a threshold, expressed as if/then, or greppable as a pattern. It must be unambiguous what the reader does next.&lt;/p&gt;

&lt;h2&gt;
  
  
  I documented Fable 5's working method along five axes
&lt;/h2&gt;

&lt;p&gt;Before getting into the "how": I maintain &lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;ccteams&lt;/a&gt;, a CLI that installs pre-built Claude Code subagent teams with one command.&lt;/p&gt;

&lt;p&gt;From v0.2.x, every team ships with two skills: a shared &lt;strong&gt;working method&lt;/strong&gt; and a per-team &lt;strong&gt;playbook&lt;/strong&gt;. Here's what actually went into them, in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Fix the work routine
&lt;/h3&gt;

&lt;p&gt;The classic Opus/Sonnet mistake is &lt;strong&gt;starting to write immediately&lt;/strong&gt; — leaning on training data or earlier context ("Go is usually written like this", "this library's API was like that") without checking the repository in front of it.&lt;/p&gt;

&lt;p&gt;So each playbook opens with a numbered procedure that forces &lt;strong&gt;pre-writing behavior&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read &lt;code&gt;go.mod&lt;/code&gt; (and its version) before using a language feature&lt;br&gt;
Read the lockfile for the actually-installed version before trusting library docs&lt;br&gt;
Read two neighboring files before picking a pattern&lt;br&gt;
&lt;code&gt;grep&lt;/code&gt; before writing a helper (it usually already exists under a different name)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In short, it pre-commits the model to one principle: &lt;strong&gt;don't decide from memory — read the repo and decide.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Define the failure patterns and their fixes
&lt;/h3&gt;

&lt;p&gt;What mid-tier models lack isn't knowledge — it's that they carry &lt;strong&gt;wrong assumptions&lt;/strong&gt;, and proceed on the premise that the plausible implementation is the correct one. Crushing that tendency is the priority.&lt;/p&gt;

&lt;p&gt;So for each team, I defined the common failure patterns in a fixed format:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;symptom → wrong instinct → correct move&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;so that when Opus/Sonnet hits the symptom, it knows exactly how to correct course.&lt;/p&gt;

&lt;p&gt;From the ccteams Next.js playbook:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One button needs an onClick&lt;/strong&gt; → put &lt;code&gt;"use client"&lt;/code&gt; at the top of the page → extract the button into its own leaf component, mark only that file client, keep the page a Server Component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From the Go playbook:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Handler hits an error&lt;/strong&gt; → call &lt;code&gt;http.Error(w, msg, 500)&lt;/code&gt; and keep going → add &lt;code&gt;return&lt;/code&gt; immediately after. &lt;code&gt;http.Error&lt;/code&gt; writes and returns; your handler doesn't — the success path below writes a second body onto a committed 500 response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are 10–15 of these per team (Go team, React Native team, Rails team, and so on). &lt;strong&gt;Naming the wrong instinct explicitly&lt;/strong&gt; lets the model recognize the exact moment it's about to make the mistake. You're patching the decision point, not the knowledge base.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Define the decision branches
&lt;/h3&gt;

&lt;p&gt;A frontier model's edge is judgment. Judgment itself can't be transplanted — but 80–90% of the recurring cases can be compiled into explicit branches. From the Go playbook:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before launching any goroutine — three questions, "nobody" is a forbidden answer:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who cancels it?&lt;/li&gt;
&lt;li&gt;Who waits for it?&lt;/li&gt;
&lt;li&gt;Where does its error go?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is "nobody", don't launch it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why this one? Because a goroutine is trivial to launch and hard to clean up. When a mid-tier model casually writes &lt;code&gt;go doWork()&lt;/code&gt;, the typical outcomes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody stops it → it keeps running after the program should be done, or lives forever (goroutine leak = memory leak)&lt;/li&gt;
&lt;li&gt;Nobody waits for it → &lt;code&gt;main&lt;/code&gt; exits first and the work silently never completes&lt;/li&gt;
&lt;li&gt;The error has nowhere to go → failures inside the goroutine get swallowed, and no one ever notices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the kind of thing that goes wrong when vibe-coding Go, so it's defined specifically for the Go team.&lt;/p&gt;

&lt;p&gt;There's also a team-independent rule shared by everyone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;State your hypothesis before touching code. A fix without a confirmed root cause is a guess.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This blocks the classic mid-tier move of &lt;strong&gt;applying a plausible-looking fix before the root cause is confirmed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For debugging specifically — where "patch the symptom's location and feel done" accidents are constant — the debug team has a dedicated gate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before writing any fix, you must be able to say: &lt;strong&gt;"X causes Y, because Z"&lt;/strong&gt; — where X and Y are observed, and Z explains why this exact symptom appears. If you can't fill in Z, you have a correlation, not a cause.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words: no causal sentence, no fix phase. You get sent back to re-verify the hypothesis.&lt;/p&gt;

&lt;p&gt;This is the pattern across ccteams: &lt;strong&gt;for each team, the decision branches that stop that team's characteristic accidents are written out explicitly.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Define the verification method
&lt;/h3&gt;

&lt;p&gt;Verification is cheap compared to generation. Which means: &lt;strong&gt;a mid-tier model plus a strong verification harness beats a mid-tier model alone, by a wide margin.&lt;/strong&gt; So every playbook contains an exact, ordered verification recipe.&lt;/p&gt;

&lt;p&gt;Not "please test it" — but which commands, in which order, and what each failure means. For Go: &lt;code&gt;go build ./...&lt;/code&gt; → &lt;code&gt;go vet ./...&lt;/code&gt; → &lt;code&gt;go test -race ./...&lt;/code&gt; (with &lt;code&gt;-race&lt;/code&gt; marked non-negotiable). The frontend playbook includes checks no linter will ever run for you: a keyboard-only walkthrough, and resize tests at 320/768/1280px.&lt;/p&gt;

&lt;p&gt;On top of that, the shared working method demands &lt;strong&gt;execution as the basis for claims&lt;/strong&gt;. A typical mid-tier shortcut is returning code that has never been run, with a cheerful "this should work". To kill that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The diff is a claim; execution is evidence. Only after actually running build/test/lint and reading their output is the claim backed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And to enforce it, every claim in a report must carry a label — &lt;code&gt;VERIFIED&lt;/code&gt; (ran it) / &lt;code&gt;REASONED&lt;/code&gt; (read the code) / &lt;code&gt;ASSUMED&lt;/code&gt; (unchecked) — with silent upgrades forbidden. You can no longer pass off "should work" as "works".&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Absorb failures one at a time with a learning loop
&lt;/h3&gt;

&lt;p&gt;Everything above was written by Fable &lt;strong&gt;in advance&lt;/strong&gt; — rules based on predicting "a mid-tier model will probably get this wrong". Not bad, but predictions are predictions. Rules grounded in &lt;strong&gt;mistakes that actually happened in your project&lt;/strong&gt; are obviously stronger.&lt;/p&gt;

&lt;p&gt;So the working method includes a &lt;strong&gt;learning loop&lt;/strong&gt;: the more you use the team, the more it optimizes itself to your project.&lt;/p&gt;

&lt;p&gt;The rule itself is simple: when a mistake surfaces that the playbook didn't anticipate, turn it into a catalog entry, and add it as a rule after human approval.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;v0.2.2&lt;/code&gt;, running &lt;code&gt;ccteams use&lt;/code&gt; automatically creates &lt;code&gt;.claude/skills/team-lessons/SKILL.md&lt;/code&gt;. Once created, this file is &lt;strong&gt;never overwritten&lt;/strong&gt; — not by team switches, not by ccteams updates. It exists to accumulate your project's own failure patterns and their fixes.&lt;/p&gt;

&lt;p&gt;When an unanticipated mistake occurs, the AI works out the &lt;strong&gt;symptom → wrong instinct → correct move&lt;/strong&gt; and asks you whether it should be recorded. On approval, the agent appends the entry to &lt;code&gt;team-lessons/SKILL.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is a team that gets smarter with every failure it absorbs.&lt;/p&gt;

&lt;p&gt;That said, bloat is unavoidable on a long-running project. As first-line countermeasures there's a duplicate check (sharpen an existing entry instead of appending a near-copy) and the human approval gate itself — but long-term operation is an open question I'm still thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making sure it actually reaches the subagents
&lt;/h2&gt;

&lt;p&gt;Claude Code skills are &lt;strong&gt;on-demand&lt;/strong&gt;: they get invoked when the model decides they're relevant. Which means a "working method" skill is never used unless something makes it relevant — and the subagents actually doing the work never invoke skills on their own, period.&lt;/p&gt;

&lt;p&gt;To solve this, I planted it in two places.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Embed it in the agent definitions
&lt;/h3&gt;

&lt;p&gt;Every agent's system prompt now begins with &lt;code&gt;FIRST ACTION: Read .claude/skills/&amp;lt;team&amp;gt;-playbook/SKILL.md and follow it&lt;/code&gt;. But "read this" alone is sometimes ignored — so as a fallback, the 5–6 most load-bearing rules from the playbook are also embedded inline in the system prompt itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Embed it at delegation time
&lt;/h3&gt;

&lt;p&gt;The orchestration rules obligate the orchestrator to paste a compressed digest of the shared working method, verbatim, into &lt;strong&gt;every&lt;/strong&gt; delegation prompt.&lt;/p&gt;

&lt;p&gt;And as the last line of defense, in case both get slipped past: the orchestrator is instructed to check every report against &lt;strong&gt;named gates&lt;/strong&gt; and reject on failure. "The build output isn't quoted? Sent back." Even if delivery fails completely, work that ignored the discipline doesn't get accepted.&lt;/p&gt;

&lt;p&gt;Why not just paste the full text everywhere? Because the more instructions you add, &lt;strong&gt;the lower the compliance per instruction&lt;/strong&gt;. So the always-loaded portion is 5–6 lines, each agent carries ~10 inline lines, and the full ~200 lines are read on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sounds like a lot of setup, right?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;ccteams&lt;/a&gt; (v0.2.x and later) already ships with Fable 5's working method handed down to Opus/Sonnet, exactly as described above. If you just want a good team quickly — or a template to study — give it a try:&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;-g&lt;/span&gt; ccteams
ccteams list
ccteams use go-api   &lt;span class="c"&gt;# or next-ts, python-fastapi, rails, django, debug — pick your team&lt;/span&gt;
&lt;span class="c"&gt;# restart Claude Code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this saved you some setup time, a star on the &lt;a href="https://github.com/toffyui/ccteams" rel="noopener noreferrer"&gt;repository&lt;/a&gt; would make my day. Issues and impressions from trying it are very welcome too!&lt;/p&gt;

&lt;h2&gt;
  
  
  Afterword
&lt;/h2&gt;

&lt;p&gt;My initial plan was to define rules for the orchestrator only. That turned out to be a dead end — because the one getting weaker &lt;em&gt;is&lt;/em&gt; the orchestrator. Fable could catch subagents' mistakes at report time with high precision and send them back; no amount of orchestrator-side rules reproduced that behavior on a weaker model.&lt;/p&gt;

&lt;p&gt;Rejection is also more expensive than prevention. So I shifted the weight onto preventing mistakes in the first place — raising the whole team — rather than catching them at the top.&lt;/p&gt;

&lt;p&gt;I reverse-engineered, shipped, and tested it, and the results were satisfying: bugs that Opus used to miss are now getting caught. For routine work, I'd say this setup has closed most of the gap between Fable and the other models.&lt;/p&gt;

&lt;p&gt;What it can't cover is Fable's unique strength: handling the unanticipated. Noticing "something is off" when nothing on any checklist says so; grasping the structure of a genuinely novel problem at a glance. That doesn't transfer through anything you can write in a skill.&lt;/p&gt;

&lt;p&gt;That remaining gap is what the learning loop above chips away at — one absorbed failure at a time.&lt;/p&gt;

&lt;p&gt;"Make Opus/Sonnet behave like Fable 5" is an idea you see floated a lot, but I rarely found articles that go into the details — so I actually did it and wrote them down. I hope some of it is useful to you.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>Use Fable 5 where it pays for itself</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:56:24 +0000</pubDate>
      <link>https://dev.to/toffy/use-fable-5-where-it-pays-for-itself-1809</link>
      <guid>https://dev.to/toffy/use-fable-5-where-it-pays-for-itself-1809</guid>
      <description>&lt;p&gt;Claude Fable 5 is the model I reach for in Claude Code when a task is ambiguous, long-running, or full of tradeoffs — planning across steps, untangling a messy goal, weighing a design choice, keeping a long session pointed in the right direction. It's genuinely good at the hard parts.&lt;/p&gt;

&lt;p&gt;Which is exactly why I stopped letting it do the easy ones. At $10 per million input tokens and $50 per million output — double Opus 4.8's $5 / $25 — spending it on boilerplate and lint fixes is like paying a principal engineer to reformat your imports. The work still needs doing. It just doesn't need &lt;em&gt;that&lt;/em&gt; person doing it.&lt;/p&gt;

&lt;p&gt;A Claude Code task is rarely one task. Take a one-liner like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add rate limiting to our API.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds small. But it unfolds into work of very different weights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deciding the algorithm — token bucket, sliding window, fixed window&lt;/li&gt;
&lt;li&gt;choosing where the counter lives — in-memory, or Redis because we run multiple instances&lt;/li&gt;
&lt;li&gt;thinking through what happens under a burst, and what a client sees on a 429&lt;/li&gt;
&lt;li&gt;writing the middleware&lt;/li&gt;
&lt;li&gt;wiring it into every route&lt;/li&gt;
&lt;li&gt;writing the tests&lt;/li&gt;
&lt;li&gt;fixing the lint and type errors&lt;/li&gt;
&lt;li&gt;checking the edge cases — clock skew, the limiter itself failing, keys never expiring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first three are real design decisions — the kind where a wrong call is expensive and annoying to unwind six months later. The rest is execution: careful work, but not work that rewards a frontier model over a merely-very-good one.&lt;/p&gt;

&lt;p&gt;So I don't try to replace Fable 5. I just keep it on the decisions and hand everything else to something cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Fable 5 the lead, not the labor
&lt;/h2&gt;

&lt;p&gt;Concretely, that means treating Fable 5 like a tech lead: it plans and delegates, and other models do the actual building.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fable 5 = orchestrator&lt;/strong&gt; — plans, decomposes, synthesizes. Writes no code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus = deep reasoning subagent&lt;/strong&gt; — architecture, complex debugging, review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet = mechanical work subagent&lt;/strong&gt; — implementation, boilerplate, tests, chores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;(Optionally) Codex = peer senior engineer&lt;/strong&gt; — an independent second opinion from a different model family&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fable 5 thinks, delegates, and stitches the results together; the cheaper models handle work they're already good at. So the pricey model is only on the clock for the steps that actually reward its judgment.&lt;/p&gt;

&lt;p&gt;The token savings were what I was after. What I didn't expect was the second effect: because Fable 5 never touches raw file contents or stack traces, its context stays clean, and it's still making sharp calls six hours into a session instead of drowning in half-finished edits.&lt;/p&gt;

&lt;p&gt;I figured that was the whole setup — three quick steps. It wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part is the subagents
&lt;/h2&gt;

&lt;p&gt;On paper, it's three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/model&lt;/code&gt; → Fable 5&lt;/li&gt;
&lt;li&gt;Create subagents with &lt;code&gt;/agents&lt;/code&gt;, pinned to Opus and Sonnet&lt;/li&gt;
&lt;li&gt;Write routing rules in your CLAUDE.md — which work goes to whom&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 1 takes ten seconds.&lt;/p&gt;

&lt;p&gt;Steps 2 and 3 are where the actual work is. Creating a subagent is fast. Creating one that pulls its weight is not.&lt;/p&gt;

&lt;p&gt;Three things I learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;description&lt;/code&gt; field decides who gets the work.&lt;/strong&gt; The orchestrator picks a subagent by reading its description. &lt;code&gt;"Use for reasoning-heavy tasks"&lt;/code&gt; is vague enough that the routing quietly drifts, and you don't notice until an answer comes back wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The system prompt decides what comes back.&lt;/strong&gt; Subagents can't see your conversation. If the prompt doesn't say what to read before acting and what to return — a conclusion, a diff, &lt;code&gt;file:line&lt;/code&gt; findings — the orchestrator ends up cleaning up after them. Which is the one thing you were trying to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Thinker" and "doer" is too few roles.&lt;/strong&gt; It's a reasonable start. But real work splits into scoping, design, implementation, verification, and shipping. Collapse verification into the builder, and you have an agent reviewing its own code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision that actually takes time, then, isn't picking models — it's writing a team of subagents whose roles are sharp enough that delegation just works. And that's a surprising amount of careful prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  ccteams: install the team design in one command
&lt;/h2&gt;

&lt;p&gt;This is the part I got tired of rewriting by hand, so I built a tool for it: &lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;ccteams&lt;/a&gt;, a &lt;strong&gt;package manager for Claude Code agent teams&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;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ccteams
ccteams list           &lt;span class="c"&gt;# see the teams&lt;/span&gt;
ccteams use generalist &lt;span class="c"&gt;# apply one to the current project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command drops a full set of role-specialized subagents into &lt;code&gt;.claude/&lt;/code&gt; — descriptions, system prompts, tool restrictions, &lt;strong&gt;and a &lt;code&gt;model:&lt;/code&gt; per agent&lt;/strong&gt; already written — &lt;strong&gt;plus the orchestration rules&lt;/strong&gt; that tell the lead session how to run them. Step 3 above (hand-writing routing rules in CLAUDE.md) ships in the box. So does the model assignment — more on that below.&lt;/p&gt;

&lt;p&gt;Teams come stack-specific out of the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generalist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stack-agnostic feature team: scope → design → build → QA → ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next-ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Next.js (App Router) + TypeScript + Tailwind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go HTTP API backends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-fastapi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FastAPI + Pydantic v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rails&lt;/code&gt; / &lt;code&gt;django&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rails / Django + DRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reproduce → root-cause → minimal fix → regression test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Technical research that writes no code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefer staying inside Claude Code? There's a plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add toffyui/ccteams
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;ccteams@ccteams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/ccteams:choose-team something for backend API work&lt;/code&gt; picks and applies the right team from a natural-language description.&lt;/p&gt;

&lt;h2&gt;
  
  
  The models are already assigned
&lt;/h2&gt;

&lt;p&gt;Here's the part I only got right recently: every bundled agent already ships with a &lt;code&gt;model:&lt;/code&gt; in its frontmatter, assigned by how much reasoning the role needs. You don't add anything. &lt;code&gt;ccteams use generalist&lt;/code&gt; lands this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Model (preset)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(main session)&lt;/td&gt;
&lt;td&gt;Orchestrator&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fable 5&lt;/strong&gt; — you pick this with &lt;code&gt;/model&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope-planner&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cut scope, pin down requirements&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;architect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Design decisions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;builder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Implementation, boilerplate, tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sonnet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qa-reviewer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verification, edge-case hunting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shipper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commits, CI, chores&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sonnet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Open any agent file and the line is right there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&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;architect&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Technical design specialist. ...&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Glob, Grep, WebSearch, WebFetch&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opus&lt;/span&gt;        &lt;span class="c1"&gt;# ← already set by ccteams&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one model ccteams doesn't touch is the lead session's — that's yours to pick with &lt;code&gt;/model&lt;/code&gt; (Fable 5). Everything below it is pinned.&lt;/p&gt;

&lt;p&gt;The split is similar to the usual “thinker / doer” setup, but the extra roles matter. Once the work is separated into scoping, design, building, review, and shipping, delegation gets much more precise — and the model choice becomes part of the role instead of a judgment the orchestrator has to remake every time.&lt;/p&gt;

&lt;p&gt;The important detail is that I do &lt;strong&gt;not&lt;/strong&gt; assign Fable 5 to roles like &lt;code&gt;architect&lt;/code&gt; or &lt;code&gt;qa-reviewer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That can look backwards at first. If planning and review are the important parts, why not put the best model there too? Because by the time a task reaches those agents, Fable 5 has already done the hardest part: turning an ambiguous user request into a clear, bounded problem that another model can solve.&lt;/p&gt;

&lt;p&gt;That is where Fable 5 earns its keep — ambiguous, long-running work full of tradeoffs. Once the question is clearly framed, Opus is usually more than enough to propose a design or review a fix. And those agents still do not get the final say. They propose; Fable 5 decides what to accept.&lt;/p&gt;

&lt;p&gt;So Fable 5 keeps control of the core judgment without spending tokens on every intermediate role. The setup does require good orchestration rules and agent prompts, but that is exactly what ccteams installs for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Codex as the peer engineer
&lt;/h2&gt;

&lt;p&gt;OpenAI's official Codex plugin lets you call Codex from inside Claude Code (install the Codex CLI on your machine first):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add openai/codex-plugin-cc
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;codex@openai-codex
/reload-plugins
/codex:setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add one paragraph to your project's CLAUDE.md (below the &lt;code&gt;@.claude/active-team.md&lt;/code&gt; import that ccteams creates):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Peer engineer&lt;/span&gt;
Codex (/codex:rescue --background) is a peer senior engineer with a different
perspective. For high-stakes decisions, task the architect agent and Codex on
the same problem in parallel and synthesize — without showing either the
other's answer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is to use Codex as a peer rather than a rubber-stamp reviewer. On a high-stakes call, I give the same problem to the Opus-pinned architect and to Codex at the same time, neither seeing the other's answer, and let Fable 5 reconcile the two. You get the most out of this when the two models come from different lineages — Opus and Codex were trained differently enough that they tend to miss different things, so one often catches what the other glossed over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to "add rate limiting"
&lt;/h2&gt;

&lt;p&gt;Remember that one-liner from the top? Here's the whole prompt I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add rate limiting to our API.
Context: Express + Redis, deployed as 3 instances behind a load balancer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I just state the goal. Claude Code routes the rest on its own — it matches the task against each agent's &lt;code&gt;description&lt;/code&gt; and delegates automatically, so Fable 5 sends the algorithm-and-storage question to &lt;code&gt;architect&lt;/code&gt;, the implementation to &lt;code&gt;builder&lt;/code&gt;, the edge cases to &lt;code&gt;qa-reviewer&lt;/code&gt;, without me naming any of them.&lt;/p&gt;

&lt;p&gt;And because each agent's model is pinned in its frontmatter, that routing decision &lt;em&gt;is&lt;/em&gt; the model decision. The moment &lt;code&gt;architect&lt;/code&gt; is chosen, the design call runs on Opus, where being wrong is expensive; &lt;code&gt;builder&lt;/code&gt;'s middleware and tests run on Sonnet. Fable 5 plans and synthesizes on a clean context and writes no code itself — so the priciest model in the room is billed only for the judgment that actually needed it.&lt;/p&gt;

&lt;p&gt;That was the goal all along: not to spend less on Fable 5, but to spend it only where it pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fable 5 is worth its price on judgment — planning, design, review — and wasteful on everything else&lt;/li&gt;
&lt;li&gt;The hard part of this setup isn't picking models, it's &lt;strong&gt;designing the team&lt;/strong&gt; that surrounds them&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;ccteams&lt;/a&gt; installs a considered team in one command — orchestration rules &lt;em&gt;and&lt;/em&gt; a per-agent &lt;code&gt;model:&lt;/code&gt; preset included, so the Fable-lead / Opus-reason / Sonnet-build split is live the moment you apply it&lt;/li&gt;
&lt;li&gt;Pick your lead model with &lt;code&gt;/model&lt;/code&gt;, restart Claude Code, and you're done — repin any agent's &lt;code&gt;model:&lt;/code&gt; line if you want a different split&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this saves you some setup, a star on &lt;a href="https://github.com/toffyui/ccteams" rel="noopener noreferrer"&gt;the repo&lt;/a&gt; is the nicest way to say so — and if the presets don't match how you'd split the work, open an issue and tell me 🙌&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>React Native team and django team are available now!</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:23:58 +0000</pubDate>
      <link>https://dev.to/toffy/react-native-team-and-django-team-are-available-now-1cag</link>
      <guid>https://dev.to/toffy/react-native-team-and-django-team-are-available-now-1cag</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e" class="crayons-story__hidden-navigation-link"&gt;One command turns Claude Code into a full dev team&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="/toffy" 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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636738%2Fe52866ed-1547-49e3-930c-4f54ea9467d3.jpeg" alt="toffy profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/toffy" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yuiko Koyanagi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yuiko Koyanagi
                
              
              &lt;div id="story-author-preview-content-3931163" 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="/toffy" 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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636738%2Fe52866ed-1547-49e3-930c-4f54ea9467d3.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yuiko Koyanagi&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/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 18&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/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e" id="article-link-3931163"&gt;
          One command turns Claude Code into a full dev team
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/claudecode"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;claudecode&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cli"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cli&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/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e" 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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

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

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>claude</category>
      <category>django</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>I built a Chrome extension that lets you blow up any webpage</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:25:11 +0000</pubDate>
      <link>https://dev.to/toffy/i-built-a-chrome-extension-that-lets-you-blow-up-any-webpage-5fdf</link>
      <guid>https://dev.to/toffy/i-built-a-chrome-extension-that-lets-you-blow-up-any-webpage-5fdf</guid>
      <description>&lt;p&gt;I built a Chrome extension called &lt;strong&gt;Site Bomb&lt;/strong&gt;. It does exactly one thing: you drop little bombs anywhere on a webpage, and the text near the blast gets physically blown apart, letter by letter.&lt;/p&gt;

&lt;p&gt;It is completely useless. It is also genuinely satisfying when you're stressed.&lt;/p&gt;

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

&lt;p&gt;Here's how it works, and the three things I had the most fun figuring out.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Turning page text into something you can blow up
&lt;/h2&gt;

&lt;p&gt;You can't apply physics to a plain text node — a paragraph is one indivisible blob as far as the DOM is concerned. So before anything explodes, Site Bomb walks the page and rewrites every visible text node, wrapping each individual character in its own element:&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;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&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;char&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="s2"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;particle style="display:inline-block;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/particle&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every letter is its own &lt;code&gt;inline-block&lt;/code&gt; box that can be moved, rotated, and flung around independently — while the page still reads normally until a bomb goes off. Whitespace is preserved so the layout doesn't collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The blast: a little bit of fake physics
&lt;/h2&gt;

&lt;p&gt;When a bomb explodes, each particle (letter) looks at its distance from the blast center and gets pushed away with a force that falls off with distance — roughly an inverse-square feel, capped so close letters don't fly off to infinity:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distX&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;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;blast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&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;distY&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;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;blast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&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;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;distX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;distX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;distY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;distY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;force&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// closer = stronger&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;force&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;force&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// clamp so it doesn't explode *too* hard&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;distY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;distY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;distance&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;velocityX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;force&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distX&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;-&lt;/span&gt;&lt;span class="mi"&gt;1&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;velocityY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;force&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;-&lt;/span&gt;&lt;span class="mi"&gt;1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every frame each letter drifts by its velocity, slowly bleeding off speed (a crude friction) and rotating as it goes, all driven through a CSS &lt;code&gt;transform&lt;/code&gt; inside a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop. It's not real physics — it just has to &lt;em&gt;feel&lt;/em&gt; explosive, and a distance-based force plus some decay gets you there surprisingly cheaply.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The bug that annoyed me for years: clicks getting eaten
&lt;/h2&gt;

&lt;p&gt;The most satisfying fix had nothing to do with explosions.&lt;/p&gt;

&lt;p&gt;Originally the click handler lived on &lt;code&gt;document.body.onclick&lt;/code&gt;. That relies on the click event &lt;strong&gt;bubbling&lt;/strong&gt; up to &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. The problem: tons of sites call &lt;code&gt;event.stopPropagation()&lt;/code&gt; on their own click handlers, so the event dies before it ever reaches &lt;code&gt;body&lt;/code&gt;. The symptom was maddening — on some sites bombs only dropped intermittently, and you'd click five times and then &lt;em&gt;all five&lt;/em&gt; would go off at once.&lt;/p&gt;

&lt;p&gt;The fix was to stop relying on bubbling and listen on the way &lt;strong&gt;down&lt;/strong&gt; instead,&lt;br&gt;
using the capture phase:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dropBomb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// useCapture — fires before any element can stopPropagation()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Capture-phase listeners run as the event travels from the root down to the target, &lt;em&gt;before&lt;/em&gt; the bubbling phase where &lt;code&gt;stopPropagation()&lt;/code&gt; usually lives. One &lt;code&gt;true&lt;/code&gt; and clicks now register instantly, everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it well-behaved
&lt;/h2&gt;

&lt;p&gt;Actually I released this a few years ago but then one day it got pulled from the Chrome Web Store because it didn't follow Chrome extension best practice.&lt;/p&gt;

&lt;p&gt;So, whilst updating Manifest from V2 to V3 this time, I also made corrections to the following minor details.&lt;/p&gt;

&lt;p&gt;A couple of small things that matter for an extension that injects into &lt;em&gt;any&lt;/em&gt; site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimal permissions.&lt;/strong&gt; It only needs &lt;code&gt;activeTab&lt;/code&gt; — temporary access to the
current tab, granted when you click the toolbar icon. No "read all your data
on every website" scare. (It's also Manifest V3, with the script injected via
&lt;code&gt;chrome.scripting&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One click to toggle.&lt;/strong&gt; Clicking the icon turns bomb mode on for that tab
(with an &lt;code&gt;ON&lt;/code&gt; badge), and clicking again turns it off — state is tracked
per tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't break the page.&lt;/strong&gt; A re-injection guard stops it from running twice,
and elements like &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; are skipped so the page
stays usable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you ever want to blow up a webpage:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://chromewebstore.google.com/detail/site-bomb/hnnabnffilimfgdcinlijkjkdemdonea?authuser=0&amp;amp;hl=ja" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/site-bomb/hnnabnffilimfgdcinlijkjkdemdonea?authuser=0&amp;amp;hl=ja&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>extensions</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One command turns Claude Code into a full dev team</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Thu, 18 Jun 2026 09:43:49 +0000</pubDate>
      <link>https://dev.to/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e</link>
      <guid>https://dev.to/toffy/one-command-turns-claude-code-into-a-full-dev-team-1d1e</guid>
      <description>&lt;p&gt;I love Claude Code's subagents. But I kept noticing the same chore: every new project, I'd hand-write the same crew again — a builder, a reviewer, someone to keep the stack conventions straight. Good setups, but they lived in one repo and never got reused.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.npmjs.com/package/ccteams" rel="noopener noreferrer"&gt;&lt;strong&gt;ccteams&lt;/strong&gt;&lt;/a&gt; — a package manager for agent teams. One command drops a ready-made team of Claude Code subagents into your project.&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;-g&lt;/span&gt; ccteams
ccteams use go-api // apply your favourite team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That applies a Go builder + reviewer, tuned for &lt;code&gt;net/http&lt;/code&gt;, to the current project. Switch when the work changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ccteams use next-ts     &lt;span class="c"&gt;# Next.js App Router + TypeScript + Tailwind&lt;/span&gt;
ccteams use generalist  &lt;span class="c"&gt;# scope -&amp;gt; design -&amp;gt; build -&amp;gt; QA -&amp;gt; ship, any stack&lt;/span&gt;
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxntrveqx01ax9bfu6ylt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxntrveqx01ax9bfu6ylt.gif" alt="CLI Demo" width="600" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Are you not sure which team you need? Don't worry, you can use &lt;code&gt;/ccteams:choose-team&lt;/code&gt; and AI will choose the best team for you!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add toffyui/ccteams
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;ccteams@ccteams
/ccteams:choose-team I want to create a todo app.
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbqeofflmsq1ocnn27sa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbqeofflmsq1ocnn27sa.gif" alt="Plugin DEMO" width="560" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a "team"?
&lt;/h2&gt;

&lt;p&gt;A team is just a curated bundle of Claude Code subagents — each a markdown file with the usual &lt;code&gt;name&lt;/code&gt; / &lt;code&gt;description&lt;/code&gt; / &lt;code&gt;tools&lt;/code&gt; frontmatter and a system prompt — plus an &lt;code&gt;orchestration.md&lt;/code&gt; that gets merged into your project's &lt;code&gt;CLAUDE.md&lt;/code&gt;. Nothing magic, nothing proprietary. It's the setup you'd build by hand, except already built and ready to reuse.&lt;/p&gt;

&lt;p&gt;ccteams ships with 8 teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;generalist&lt;/strong&gt; — stack-agnostic, takes a feature scope → design → build → QA → ship&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;next-ts&lt;/strong&gt; — Next.js App Router + TypeScript + Tailwind&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;frontend&lt;/strong&gt; — framework-agnostic UI/UX and accessibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;go-api&lt;/strong&gt; — idiomatic Go HTTP APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;python-fastapi&lt;/strong&gt; — FastAPI + Pydantic v2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rails&lt;/strong&gt; — Ruby on Rails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;debug&lt;/strong&gt; — reproduce → root-cause → fix → regression test&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;research&lt;/strong&gt; — compares options and recommends; writes no code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;use&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;No black box. &lt;code&gt;ccteams use &amp;lt;team&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copies the team's agents into &lt;code&gt;.claude/agents/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;.claude/active-team.md&lt;/code&gt; and adds an &lt;code&gt;@.claude/active-team.md&lt;/code&gt; import to your &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tracks everything in &lt;code&gt;.claude/.ccteams-manifest.json&lt;/code&gt; so switching teams is clean&lt;/li&gt;
&lt;li&gt;Won't overwrite agents you wrote by hand — there's a collision guard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Switching to another team removes only what ccteams placed last time. Your own files are never touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one catch: restart
&lt;/h2&gt;

&lt;p&gt;Being honest, because this trips people up: &lt;strong&gt;Claude Code loads subagents at session start.&lt;/strong&gt; So after &lt;code&gt;ccteams use&lt;/code&gt;, you have to restart Claude Code for the team to take effect. It is not instant. Run the command, then &lt;code&gt;/exit&lt;/code&gt; and relaunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: do it from inside Claude Code
&lt;/h2&gt;

&lt;p&gt;There's an optional plugin that adds slash commands, so you don't have to leave Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add toffyui/ccteams
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;ccteams@ccteams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/ccteams:list-teams&lt;/code&gt;, &lt;code&gt;/ccteams:use-team &amp;lt;team&amp;gt;&lt;/code&gt;, and the fun one — &lt;code&gt;/ccteams:choose-team&lt;/code&gt; — where you describe what you need in plain English and it picks the team for you. The plugin is optional; the CLI works fine on its own.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbqeofflmsq1ocnn27sa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbqeofflmsq1ocnn27sa.gif" alt="Plugin DEMO" width="560" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Power users: parallel teammates
&lt;/h2&gt;

&lt;p&gt;If you want the team members to run in parallel and message each other instead of one-at-a-time delegation, opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ccteams use generalist &lt;span class="nt"&gt;--agent-teams&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turns on Claude Code's experimental agent-teams mode for the project. It's experimental, so treat it as a power-user option, not the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&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; &lt;span class="nt"&gt;-g&lt;/span&gt; ccteams
ccteams list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's open source (MIT): &lt;strong&gt;github.com/toffyui/ccteams&lt;/strong&gt;. If it's useful, a star helps a lot.&lt;/p&gt;

&lt;p&gt;One real question for you: &lt;strong&gt;which stack should I add a team for next?&lt;/strong&gt; I'm weighing Spring Boot, Django, and SwiftUI — tell me in the comments what you'd actually use.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>opensource</category>
      <category>cli</category>
    </item>
    <item>
      <title>I built “Mozary”: An iOS App That Turns Your Photos Into a Zoomable Mosaic</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Fri, 13 Mar 2026 15:42:56 +0000</pubDate>
      <link>https://dev.to/toffy/i-built-mozary-an-ios-app-that-turns-your-photos-into-a-zoomable-mosaic-46d1</link>
      <guid>https://dev.to/toffy/i-built-mozary-an-ios-app-that-turns-your-photos-into-a-zoomable-mosaic-46d1</guid>
      <description>&lt;p&gt;Mozary is an iOS app that turns &lt;strong&gt;hundreds of your photos&lt;/strong&gt; into a single &lt;strong&gt;zoomable mosaic&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom out → it looks like one image
&lt;/li&gt;
&lt;li&gt;Zoom in → individual photos appear (and you rediscover forgotten moments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App Link: &lt;a href="https://apps.apple.com/us/app/mozary/id6759013945" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/mozary/id6759013945&lt;/a&gt;&lt;br&gt;
Try it out!&lt;/p&gt;

&lt;p&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.linkedin.com/posts/yuiko-koyanagi-444283a7_buildinpublic-mobileapp-computervision-ugcPost-7433995703770824704-c1bi?utm_source=share&amp;amp;amp%3Butm_medium=member_desktop&amp;amp;amp%3Brcm=ACoAABakprQBOyWNcpBLommvWlWDoxRDHll6T4c" 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%2Fdms.licdn.com%2Fplaylist%2Fvid%2Fv2%2FD4D05AQHcKCO8qe7akg%2Fthumbnail-with-play-button-overlay-high%2FB4DZyrbtFRHADU-%2F0%2F1772402695142%3Fe%3D2147483647%26v%3Dbeta%26t%3DISUpjeQ953P6p79EsVSMBkz9X0I6FlxjtlSHo_Ommgk" height="auto" class="m-0"&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.linkedin.com/posts/yuiko-koyanagi-444283a7_buildinpublic-mobileapp-computervision-ugcPost-7433995703770824704-c1bi?utm_source=share&amp;amp;amp%3Butm_medium=member_desktop&amp;amp;amp%3Brcm=ACoAABakprQBOyWNcpBLommvWlWDoxRDHll6T4c" rel="noopener noreferrer" class="c-link"&gt;
            #buildinpublic #mobileapp #computervision #imageprocessing #productdevelopment | Yuiko Koyanagi
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            I’ve been building a photo mosaic app that creates an image from images.
You pick a target photo, and the app composes it using many smaller photos—so from a distance it looks like one picture, but when you zoom in, you can still see each individual photo clearly.
I put a lot of work into the processing pipeline, and the generation is genuinely fast (the video isn’t sped up).
Now it’s in app review—excited to share more once it’s live!

#buildinpublic #mobileapp #computervision #imageprocessing #productdevelopment
          &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%2Fstatic.licdn.com%2Faero-v1%2Fsc%2Fh%2Fal2o9zrvru7aqj8e1x2rzsrca"&gt;
          linkedin.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&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%2Fzo8mwxj6cgrjtnb3r4d5.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%2Fzo8mwxj6cgrjtnb3r4d5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea (why I built it)
&lt;/h2&gt;

&lt;p&gt;I take a lot of photos, but I rarely revisit them.&lt;br&gt;&lt;br&gt;
I wanted a way to turn “a pile of photos” into &lt;strong&gt;something you actually want to look at&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During a trip, I saw a huge mosaic wall and thought: &lt;em&gt;what if my own memories could become a mosaic artwork?&lt;/em&gt;&lt;br&gt;&lt;br&gt;
That became &lt;strong&gt;Mozary&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (user flow)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Choose a &lt;strong&gt;target image&lt;/strong&gt; (what the mosaic becomes)&lt;/li&gt;
&lt;li&gt;Choose photos from your library to use as tiles (up to &lt;strong&gt;1000&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Generate (usually &lt;strong&gt;5–20s&lt;/strong&gt;, up to &lt;strong&gt;~40s&lt;/strong&gt; for 1000 photos)&lt;/li&gt;
&lt;li&gt;Pinch to zoom, then export as an image/video&lt;/li&gt;
&lt;/ol&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%2Fhum4gv4bvvdqo23h8rm9.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%2Fhum4gv4bvvdqo23h8rm9.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering part: making the viewer &lt;em&gt;buttery smooth&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn’t generating the mosaic.&lt;/p&gt;

&lt;p&gt;It was &lt;strong&gt;rendering&lt;/strong&gt; it interactively.&lt;/p&gt;

&lt;p&gt;A mosaic like &lt;strong&gt;200×150&lt;/strong&gt; already means &lt;strong&gt;30,000 tiles&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Naively drawing them every frame is a fast path to jank (or crashes).&lt;/p&gt;

&lt;h3&gt;
  
  
  Borrowing an idea from Google Maps: LOD (Level of Detail)
&lt;/h3&gt;

&lt;p&gt;Google Maps doesn’t draw every tiny detail at every zoom level.&lt;br&gt;&lt;br&gt;
It switches what it renders depending on zoom.&lt;/p&gt;

&lt;p&gt;Mozary uses a similar &lt;strong&gt;LOD&lt;/strong&gt; approach (3 levels):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoomed out:&lt;/strong&gt; draw a &lt;strong&gt;single pre-rendered image&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Instead of looping through 30,000 tiles, I pack each tile color into a pixel and render &lt;em&gt;one&lt;/em&gt; image.&lt;br&gt;&lt;br&gt;
Same look, dramatically cheaper.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoomed in:&lt;/strong&gt; render &lt;strong&gt;only what’s visible&lt;/strong&gt;, in &lt;strong&gt;256×256px tiles&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I issue render requests only for the region currently on screen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep the main thread free:&lt;/strong&gt; heavy rendering runs on a &lt;strong&gt;background thread&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
So pan/zoom stays responsive.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: the mosaic scrolls/zooms smoothly even at large sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color matching: RGB → Lab
&lt;/h2&gt;

&lt;p&gt;Mosaic quality is mostly about color matching.&lt;/p&gt;

&lt;p&gt;I first tried RGB distance, but it often chose tiles that were numerically close yet visually off.&lt;br&gt;&lt;br&gt;
RGB isn’t perceptually uniform.&lt;/p&gt;

&lt;p&gt;Switching to &lt;strong&gt;Lab color space&lt;/strong&gt; improved matches immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert each photo’s average color &lt;strong&gt;RGB → Lab&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;For each grid cell, pick the tile with the smallest &lt;strong&gt;Euclidean distance&lt;/strong&gt; in Lab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple change, big visual improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: Color grading when your photo set lacks colors
&lt;/h2&gt;

&lt;p&gt;Sometimes your selected photos don’t cover the target’s color palette.&lt;/p&gt;

&lt;p&gt;So I added an optional “color grading overlay”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overlay the target’s &lt;strong&gt;hue + saturation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;keep the tiles’ &lt;strong&gt;luminance&lt;/strong&gt; (so the photo texture stays)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s subtle when your set already has enough color, but helpful when it doesn’t:&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%2Fbva1hw2x7jt0vz5kshgk.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%2Fbva1hw2x7jt0vz5kshgk.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Speed tricks that mattered
&lt;/h2&gt;

&lt;p&gt;Two practical optimizations made generation fast enough for real use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize&lt;/strong&gt; photo analysis (average color computation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downsample&lt;/strong&gt; for analysis (I use ~&lt;strong&gt;40×40px&lt;/strong&gt; instead of full-res)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if photos are several MB, the color info needed for matching is captured at small size—saving both memory and time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to improve next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reduce repeated use of the same tile photo (more even distribution)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  That's it!
&lt;/h2&gt;

&lt;p&gt;If you try it, I’d love feedback—especially on export quality and performance on older devices.&lt;/p&gt;

&lt;p&gt;App Link: &lt;a href="https://apps.apple.com/us/app/mozary/id6759013945" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/mozary/id6759013945&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>performance</category>
      <category>imageprocessing</category>
      <category>swift</category>
    </item>
    <item>
      <title>Hosting Applications with AWS CDK: An Introduction to Infrastructure as Code</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Wed, 16 Oct 2024 10:18:27 +0000</pubDate>
      <link>https://dev.to/toffy/hosting-applications-with-aws-cdk-an-introduction-to-infrastructure-as-code-3c9b</link>
      <guid>https://dev.to/toffy/hosting-applications-with-aws-cdk-an-introduction-to-infrastructure-as-code-3c9b</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Until now, I have been manually building the infrastructure for my applications. This time, I challenged myself to manage the infrastructure using Infrastructure as Code (IaC).&lt;/p&gt;

&lt;p&gt;By utilizing AWS CDK, I was able to codify complex infrastructure settings, manage them simply, and enable efficient deployments. In this article, I will introduce how to host an application using IaC with AWS CDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the Application Architecture
&lt;/h2&gt;

&lt;p&gt;The application I created this time has the following structure. I designed it to efficiently run both the frontend and backend using Docker images on AWS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: React + TypeScript&lt;/li&gt;
&lt;li&gt;Backend: NestJS + TypeScript
Below is a diagram of the application's architecture:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fp7pud0kd121snakkx9zs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fp7pud0kd121snakkx9zs.png" alt="a diagram of the application's architecture" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure Design
&lt;/h2&gt;

&lt;p&gt;First, I needed to create a secure network configuration using a VPC (Virtual Private Cloud). Therefore, I set up public and private subnets within the VPC.&lt;/p&gt;

&lt;p&gt;I placed an ALB (Application Load Balancer) in the public subnet and the backend and frontend servers managed by ECS (Elastic Container Service) Fargate in the private subnet. This ensures that the servers are not directly exposed to the outside, enhancing security.&lt;/p&gt;

&lt;p&gt;Users can access the application via the Internet Gateway. Access is received through the ALB placed in the public subnet. The ALB is designed to route requests to the appropriate services and perform load balancing.&lt;/p&gt;

&lt;p&gt;For the database, I decided to use DynamoDB to store data. The ECS tasks in the private subnet are configured to access DynamoDB via VPC endpoints. This allows communication with DynamoDB without going through the internet, improving communication security and performance.&lt;/p&gt;

&lt;p&gt;The Docker images used by the servers are stored in ECR (Elastic Container Registry). ECS uses these images to run the application. Additionally, by obtaining images from S3 and ECR via VPC endpoints, it is possible to use resources within the private network.&lt;/p&gt;

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

&lt;p&gt;Next, I will translate the above infrastructure design into code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;First, set up the authentication information using AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, proceed with the CDK setup:&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;-g&lt;/span&gt; aws-cdk
&lt;span class="nb"&gt;mkdir &lt;/span&gt;cdk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;cdk  &lt;span class="c"&gt;# Create a directory of your choice and move into it&lt;/span&gt;
cdk init app &lt;span class="nt"&gt;--language&lt;/span&gt; typescript
cdk bootstrap aws://&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCOUNT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically generates the files necessary for CDK settings.&lt;/p&gt;

&lt;p&gt;Prepare Docker images for both the frontend and backend and store them in ECR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing the CDK Content
&lt;/h3&gt;

&lt;p&gt;Modify &lt;code&gt;cdk-stack.ts&lt;/code&gt; Since the CdkStack class is already created, write the above design into its constructor. I want to create various environments such as testing and production, so I add &lt;code&gt;environment&lt;/code&gt; as an argument to the constructor for future use.&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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CdkStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&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="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="nx"&gt;environment&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="c1"&gt;// Added&lt;/span&gt;
    &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&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 &lt;code&gt;CdkStack&lt;/code&gt; class is called in cdk.ts, so modify the code in &lt;code&gt;cdk.ts&lt;/code&gt; to make environment usable:&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="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;source-map-support/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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CdkStack&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="s2"&gt;../lib/cdk-stack&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;App&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;environment&lt;/span&gt; &lt;span class="o"&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryGetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Obtain from command line during deployment&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CdkStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;CdkStack`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;);&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;synth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, write the content inside the constructor of the CdkStack class in cdk-stack.ts.&lt;/p&gt;

&lt;p&gt;First, create a VPC. Since we need private and public subnets this time, write as follows:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vpc&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppVpc`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;vpcName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppVpc`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxAzs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;natGateways&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="na"&gt;subnetConfiguration&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;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="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppPublicSubnet`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cidrMask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&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;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="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppPrivateSubnet`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cidrMask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&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;Regarding the Internet Gateway, since we are creating a public subnet, it is automatically created even though it is not explicitly mentioned in the code.&lt;/p&gt;

&lt;p&gt;Next, create VPC endpoints that the private subnet will use:&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;// VPC Endpoint for DynamoDB&lt;/span&gt;
&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGatewayEndpoint&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;DynamoDbEndpoint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InterfaceVpcEndpointAwsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DYNAMODB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// VPC Endpoint for S3 (necessary for obtaining Docker images)&lt;/span&gt;
&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGatewayEndpoint&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;S3Endpoint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GatewayVpcEndpointAwsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// VPC Endpoint for ECR (endpoint to send requests)&lt;/span&gt;
&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterfaceEndpoint&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;EcrApiEndpoint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InterfaceVpcEndpointAwsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ECR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// VPC Endpoint for ECR Docker (endpoint to obtain Docker images)&lt;/span&gt;
&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterfaceEndpoint&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;EcrDockerEndpoint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InterfaceVpcEndpointAwsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ECR_DOCKER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// VPC Endpoint for CloudWatch Logs (for logging)&lt;/span&gt;
&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterfaceEndpoint&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;CloudWatchLogsEndpoint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InterfaceVpcEndpointAwsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDWATCH_LOGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&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;All connections are with the private subnet, but for DynamoDB and S3, we can use a&lt;code&gt;ddGatewayEndpoint (gateway-type endpoints)&lt;/code&gt;, so specifying the subnet is unnecessary.&lt;/p&gt;

&lt;p&gt;Next, create ECS, which is necessary for container management:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Cluster&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppCluster`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;clusterName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppCluster`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vpc&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;Create an IAM role for Fargate to use:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskExecutionRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;TaskExecutionRole`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ensure only ECS tasks can assume this role&lt;/span&gt;
    &lt;span class="na"&gt;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ecs-tasks.amazonaws.com&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="c1"&gt;// Attach necessary access permissions&lt;/span&gt;
&lt;span class="nx"&gt;taskExecutionRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAwsManagedPolicyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service-role/AmazonECSTaskExecutionRolePolicy&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Fargate task definition for the frontend:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontendTaskDef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateTaskDefinition&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendTaskDef`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;family&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendTaskDef`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;memoryLimitMiB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executionRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskExecutionRole&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;Since we are using ECR, specify the image repository for the frontend (unnecessary if using Docker Hub):&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontendRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ecr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromRepositoryName&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;frontendRepository`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-react-nginx-image&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;p&gt;Add container information to the task definition created above. This time, we specify the image repository:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontendContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frontendTaskDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addContainer&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendContainer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;containerName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendContainer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ContainerImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEcrRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;frontendRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwsLogDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;streamPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Frontend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Prefix for logging&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;frontendContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPortMappings&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TCP&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;The &lt;code&gt;containerPort&lt;/code&gt; refers to the port for access from the ALB to Fargate, so it can remain as 80 regardless of whether the app itself is hosted with HTTPS.&lt;/p&gt;

&lt;p&gt;Next, launch a Fargate service for the frontend server:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontendService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateService&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendService`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;serviceName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendService`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frontendTaskDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;assignPublicIp&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="na"&gt;vpcSubnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&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;// Configure auto-scaling&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scalingFrontend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frontendService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autoScaleTaskCount&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;minCapacity&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="na"&gt;maxCapacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Auto-scale when CPU utilization exceeds 50%&lt;/span&gt;
&lt;span class="nx"&gt;scalingFrontend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaleOnCpuUtilization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CpuScalingFrontend&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;targetUtilizationPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Auto-scale when memory utilization exceeds 70%&lt;/span&gt;
&lt;span class="nx"&gt;scalingFrontend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaleOnMemoryUtilization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MemoryScalingFrontend&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;targetUtilizationPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&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;With this, the settings for the frontend server are complete. Next, proceed to set up the backend server. Since it is almost the same as the frontend server, explanations will be brief.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backendTaskDef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateTaskDefinition&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendTaskDef`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;family&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendTaskDef`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;memoryLimitMiB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executionRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskExecutionRole&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;backendRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ecr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromRepositoryName&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;backendRepository`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-nestjs-image&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;backendContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;backendTaskDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addContainer&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendContainer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;containerName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendContainer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ContainerImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEcrRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;backendRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwsLogDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;streamPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Backend&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;span class="nx"&gt;backendContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPortMappings&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TCP&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;backendService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateService&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendService`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;serviceName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendService`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backendTaskDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;assignPublicIp&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="na"&gt;vpcSubnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;subnetType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_ISOLATED&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scalingBackend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;backendService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autoScaleTaskCount&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;minCapacity&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="na"&gt;maxCapacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;scalingBackend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaleOnCpuUtilization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CpuScalingBackend&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;targetUtilizationPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;scalingBackend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaleOnMemoryUtilization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MemoryScalingBackend&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;targetUtilizationPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&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;Next, create the ALB:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadBalancer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApplicationLoadBalancer&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppApplicationLoadBalancer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;loadBalancerName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppApplicationLoadBalancer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;internetFacing&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect the ALB to resources in the private subnet:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FrontendTargetGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApplicationTargetGroup&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendTG`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;targetGroupName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppFrontendTG`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;vpc&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;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApplicationProtocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;frontendService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&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="s2"&gt;/&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backendTargetGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApplicationTargetGroup&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendTG`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;targetGroupName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;AppBackendTG`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;vpc&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;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApplicationProtocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;backendService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&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="s2"&gt;/v1/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Specify the API for health checks&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;Describe the ALB's request handling:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;loadBalancer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;HttpListener`&lt;/span&gt;&lt;span class="p"&gt;,&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;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;open&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;defaultAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ListenerAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;FrontendTargetGroup&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTargetGroups&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;BackendTargetGroups`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;targetGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;backendTargetGroup&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;priority&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="na"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ListenerCondition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathPatterns&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, we are using a single ALB to distribute traffic to the frontend and backend. By default, requests are forwarded to the frontend, but if there is access with /v1, it is set to forward to the backend Fargate. If you are using two ALBs, the addTargetGroups configuration is unnecessary.&lt;/p&gt;

&lt;p&gt;Finally, create the necessary DynamoDB table. In DynamoDB, only the PartitionKey is mandatory, but you can include SortKey, Index, and TTL as 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usersTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;UsersTable`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tableName&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;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-users`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partitionKey&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="s2"&gt;account_id&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;usersTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantReadWriteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backendTaskDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskRole&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have written the code necessary for IaC, we just need to execute and verify it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution
&lt;/h3&gt;

&lt;p&gt;First, check for syntax errors in the CDK code above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk synth &lt;span class="nt"&gt;--context&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are errors, they will appear as follows, so fix them as needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: Validation failed with the following errors:
  &lt;span class="o"&gt;[&lt;/span&gt;undefinedCdkStack/undefinedAppFrontendTargetGroup] Target group name: &lt;span class="s2"&gt;"undefinedAppFrontendTargetGroup"&lt;/span&gt; can have a maximum of 32 characters.
  &lt;span class="o"&gt;[&lt;/span&gt;undefinedCdkStack/undefinedAppBackendTargetGroup] Target group name: &lt;span class="s2"&gt;"undefinedAppBackendTargetGroup"&lt;/span&gt; can have a maximum of 32 characters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can execute without errors, proceed to deploy. If the Docker image is not functioning properly, Fargate will repeatedly start up, encounter errors, shut down, and restart. If it is taking an excessive amount of time, please check the AWS Management Console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk deploy &lt;span class="nt"&gt;--context&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, hosting the application using IaC is complete.&lt;/p&gt;

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

&lt;p&gt;This was my first attempt at IaC, and by managing the infrastructure as code, I found it very beneficial to build infrastructure for multiple environments such as production and development without human errors.&lt;/p&gt;

&lt;p&gt;If this article was helpful in any way, I would be encouraged if you could press the like button! Thank you for reading to the end.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>cdk</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How to Automatically Adjust Colors in High Contrast Mode</title>
      <dc:creator>Yuiko Koyanagi</dc:creator>
      <pubDate>Thu, 22 Aug 2024 14:10:05 +0000</pubDate>
      <link>https://dev.to/toffy/how-to-automatically-adjust-colors-in-high-contrast-mode-p4h</link>
      <guid>https://dev.to/toffy/how-to-automatically-adjust-colors-in-high-contrast-mode-p4h</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I recently received a bug report where an SVG icon was not displaying correctly in high contrast mode. In this article, I’ll share the solution that worked for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;In high contrast mode, I used the CanvasText system color to automatically adjust the icon's color.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;mask-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url(svg-link)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forced-colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.icon&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CanvasText&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;In my case, I initially used &lt;code&gt;currentColor&lt;/code&gt; to inherit the color from the parent element. However, in high contrast mode, I wanted to set the &lt;code&gt;background-color&lt;/code&gt; to &lt;code&gt;CanvasText&lt;/code&gt; universally within the child element, so I applied this change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CanvasText?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CanvasText&lt;/code&gt; refers to the text color used for application content or documents. It automatically adjusts to provide the best contrast against the system's background color.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;CanvasText&lt;/code&gt;, you ensure that text and icons remain visible even when the user enables high contrast mode. Additionally, since &lt;code&gt;CanvasText&lt;/code&gt; adapts based on the system's theme, it works well with both dark and light modes.&lt;/p&gt;

&lt;p&gt;In my case, the icon's &lt;code&gt;background-color&lt;/code&gt; was initially set to black. However, when the background turned black in high contrast mode, the icon became invisible. Changing the color to white made it visible again, but to handle this consistently across all scenarios, I opted to use the system color &lt;code&gt;CanvasText&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/system-color" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/system-color&lt;/a&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>css</category>
    </item>
  </channel>
</rss>
