<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Sravan Karuturi - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Sravan Karuturi - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 05 May 2026 14:46:37 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/sravankaruturi/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Rate Limiter with Redis and Python to Scale Your Apps ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever built a web application, you know that without a proper mechanism to control traffic, your application can become overwhelmed, leading to slow response times, server crashes, and a poor user experience. Even worse, it can leave you vul... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-rate-limiter-with-redis-and-python/</link>
                <guid isPermaLink="false">68dfe6e0dcc5f825f4d48c85</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cybersecurity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sravan Karuturi ]]>
                </dc:creator>
                <pubDate>Fri, 03 Oct 2025 15:08:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759503803144/4d974610-95dc-4db8-989a-0d705dc4d431.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever built a web application, you know that without a proper mechanism to control traffic, your application can become overwhelmed, leading to slow response times, server crashes, and a poor user experience. Even worse, it can leave you vulnerable to Denial-of-Service (DoS) attacks. This is where rate limiting comes in.</p>
<p>In this tutorial, you’ll build a distributed rate limiter. This is the kind of system you need when your application is deployed across multiple servers or virtual machines, and you need to enforce a global limit on all incoming requests.</p>
<p>You’ll build a simple URL shortener application and then implement a robust rate limiter for it using a powerful and efficient combination of tools:</p>
<ul>
<li><p>Python and Flask for your web application.</p>
</li>
<li><p>Redis as your high-speed, centralized data store for tracking requests.</p>
</li>
<li><p>Terraform and Proxmox to define and provision your virtual machine infrastructure.</p>
</li>
<li><p>Docker to containerize your application for easy deployment.</p>
</li>
<li><p>Nginx as a load balancer to distribute traffic across your app servers.</p>
</li>
<li><p>k6 to load-test your system and prove that your rate limiter actually works.</p>
</li>
</ul>
<p>This is intended for new developers learning about various system design concepts or for experts who just want a refresher.</p>
<p>By the end of this guide, you'll understand not just the code, but the complete system architecture required to deploy a scalable, resilient application.</p>
<p>Let's get started!</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>While not absolutely required to follow along, I’d recommend setting up a Proxmox server on an old laptop to implement the topics you learn and code along with the article. I recommend this <a target="_blank" href="https://www.youtube.com/watch?v=5j0Zb6x_hOk&amp;list=PLT98CRl2KxKHnlbYhtABg6cF50bYa8Ulo">YouTube playlist</a> for getting started. Please note that I am in no way affiliated with this channel. I just found it helpful for me.</p>
<p>However, If you do not have a local Proxmox server, you can skip that part and just follow along to understand how a rate limiter is built and how it is set up to properly work with multiple servers.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-big-picture-our-system-architecture">The Big Picture: Our System Architecture</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-how-to-define-the-infrastructure-with-terraform">Step 1: How to Define the Infrastructure with Terraform</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-how-to-implement-the-rate-limiter-logic-in-python">Step 2: How to Implement the Rate Limiter Logic in Python</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-containerizing-and-testing">Step 3: Containerizing and Testing</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-the-big-picture-our-system-architecture">The Big Picture: Our System Architecture</h2>
<p>Before we dive into the code, let's look at the architecture we're building. I will be using <a target="_blank" href="https://www.proxmox.com/en/products/proxmox-virtual-environment/overview">Proxmox Virtual Environment</a> to setup a server cluster just like you would have in a datacenter.</p>
<h3 id="heading-how-to-set-up-proxmox">How to Set Up Proxmox</h3>
<p><code>Proxmox Virtual Environment</code> is an open source platform for virtualization. It lets you manage multiple VMs, ccontainers and other clusters with ease. For instance, I turned my old gaming computer into a Proxmox server which lets me run more than 20 virtual machines on it at the same time, making it similar to my very own datacenter. This lets me experiment with distributed applications by simulating datacenter environments.</p>
<p>To setup your own cluster, all you need is an old computer. You can download the ISO image from <a target="_blank" href="https://www.proxmox.com/en/downloads">here</a> and boot from the USB drive. Once you install it, you can configure the host machine via a web browser on any other computer on the same network.</p>
<p>For example, my proxmox server is located at <code>10.0.0.108</code> and I can access it via the browser on my laptop.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759194790299/35e9363f-b739-4085-a589-c1bafbac0504.png" alt="Example Proxmox cluster" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>We define all our virtual machines in our <code>main.tf</code> file. And run a simple command <code>terraform apply</code> to spin these servers up. For more reading on how to use Terraform with Proxmox, I recommend this <a target="_blank" href="https://spacelift.io/blog/terraform-proxmox-provider">blog post</a></p>
<p>Back to our use case, we’ll have a few virtual machines that will serve as different kinds of servers:</p>
<ol>
<li><p>A Load balancer</p>
</li>
<li><p>A Rate Limiter ( A Redis Cache )</p>
</li>
<li><p>Two Web Servers</p>
</li>
<li><p>A Postgres database</p>
</li>
<li><p>One Virtual Machine that will test the load by simulating hundreds of calls per minute.</p>
</li>
</ol>
<p>If all of this seems daunting, don’t worry too much about it. You don’t need to set all this up to follow along.</p>
<h3 id="heading-centralized-rate-limiter">Centralized Rate Limiter</h3>
<p>Since our application will run on multiple servers (or "nodes"), we can't store request counts in memory on each individual server. Why? Because each server would have its own separate count, and we wouldn't have a <em>global</em> rate limit.</p>
<p>The solution is to use a centralized data store that all our application nodes can access. This is where Redis comes in.</p>
<p>Here’s a diagram of our setup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758476002871/1d70ce5b-e19c-4d7d-9c0b-cc18840a07bf.png" alt="A Small diagram depicting the architecture we'll form with all these virtual nodes" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<ol>
<li><p>User requests first hit our Nginx load balancer.</p>
</li>
<li><p>The load balancer distributes the traffic evenly between our two web server VMs. The configuration is simple, using an upstream block to define the servers.</p>
</li>
<li><p>Each web server runs our Python Flask application inside a Docker container.</p>
</li>
<li><p>Before processing any request, the Flask app communicates with the central Redis rate limiter VM to check if the user has exceeded the rate limit.</p>
</li>
<li><p>If the user is within the limit, the app processes the request and interacts with the PostgreSQL Database. If they're over the limit, it sends back a “429 Too Many Requests” error.</p>
</li>
</ol>
<p>This architecture ensures that no matter which web server handles the request, the rate limit is checked against the same, shared data source.</p>
<h2 id="heading-step-1-how-to-define-the-infrastructure-with-terraform"><strong>Step 1: How to Define the Infrastructure with Terraform</strong></h2>
<p>Manually setting up multiple virtual machines can be tedious and prone to errors. That's why we use Terraform, an Infrastructure as Code (IaC) tool. It lets us define our entire infrastructure in configuration files.</p>
<p><strong>Note</strong>: You can skip this section if you just want to see the rate limiter in action and how it’s used.</p>
<p>Our <a target="_blank" href="https://github.com/sravankaruturi/system-design/blob/main/infra/main.tf">main.tf</a> file defines all the components of our system. Let's look at a key piece: the Redis VM.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># --- Redis Cache for Rate Limiter ---</span>
<span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_vm_qemu"</span> <span class="hljs-string">"redis_cache"</span> {

    <span class="hljs-string">vmid</span>        <span class="hljs-string">=</span> <span class="hljs-number">130</span>
    <span class="hljs-string">name</span>        <span class="hljs-string">=</span> <span class="hljs-string">"redis-cache-rate-limiter"</span>
    <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">"pve"</span>
    <span class="hljs-string">agent</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
    <span class="hljs-string">cores</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
    <span class="hljs-string">memory</span>      <span class="hljs-string">=</span> <span class="hljs-number">1024</span>
    <span class="hljs-comment"># ... cloud-init config ...</span>
    <span class="hljs-string">ipconfig0</span>  <span class="hljs-string">=</span> <span class="hljs-string">"ip=10.0.0.130/24,gw=10.0.0.1"</span>
    <span class="hljs-comment"># ... disk and network config ...</span>

    <span class="hljs-comment"># 1. Install Docker</span>
    <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
        <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
            <span class="hljs-string">"sleep 30; sudo apt-get update -y"</span>,
            <span class="hljs-string">"sudo apt-get install -y docker.io docker-compose"</span>,
            <span class="hljs-string">"sudo mkdir -p /opt/redis"</span>
        ]
    }

    <span class="hljs-comment"># 2. Upload docker-compose file</span>
    <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
         <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"files/redis-docker-compose.yml"</span>
         <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/docker-compose.yml"</span>
    }

    <span class="hljs-comment"># 3. Move file and run docker-compose</span>
    <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
        <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
            <span class="hljs-string">"sudo mv /home/${var.ssh_user}/docker-compose.yml /opt/redis/docker-compose.yml"</span>,
            <span class="hljs-string">"cd /opt/redis &amp;&amp; sudo docker-compose up -d"</span>
        ]
    }
}
</code></pre>
<p>This block tells Terraform to create a <code>Proxmox QEMU virtual machine</code> with a specific IP address <code>(10.0.0.130)</code>. After the VM is created, it uses provisioners to connect via SSH and run commands. Here, it installs Docker, uploads our <code>redis-docker-compose.yml file</code>, and starts the Redis container.</p>
<p>The <code>redis-docker-compose.yml</code> itself is very straightforward:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.8'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">redis_cache</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6379:6379"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redisdata:/data</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">redisdata:</span>
</code></pre>
<p>This ensures we have a persistent, containerized Redis instance ready to serve our application. The Terraform configuration similarly defines our web servers, load balancer, and databases.</p>
<h2 id="heading-step-2-how-to-implement-the-rate-limiter-logic-in-python"><strong>Step 2: How to Implement the Rate Limiter Logic in Python</strong></h2>
<p>Now, for the heart of our system: the Python code that implements the rate limiting logic. We're using a sophisticated and memory-efficient algorithm called the Sliding Window Log.</p>
<p>The idea is simple: for each user, we keep a log of the timestamps of their recent requests. We store this log in a Redis Sorted Set.</p>
<p>Let's break down the code from <a target="_blank" href="https://github.com/sravankaruturi/system-design/blob/main/web-servers/app.py"><code>app.py</code></a>.</p>
<h3 id="heading-the-flask-appbeforerequest-hook"><strong>The Flask</strong> <code>@app.before_request</code> <strong>Hook</strong></h3>
<p>Flask allows us to run code before any request is handled by its intended view function. This is the perfect place to put our rate limiter.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> psycopg2
<span class="hljs-keyword">import</span> string
<span class="hljs-keyword">import</span> random
<span class="hljs-keyword">import</span> redis
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask, request, redirect, jsonify

app = Flask(__name__)

<span class="hljs-comment"># --- Database Connection Details ---</span>
DB_HOST = <span class="hljs-string">"10.0.0.200"</span> 
DB_NAME = <span class="hljs-string">"urldb"</span>
DB_USER = <span class="hljs-string">"myuser"</span>
DB_PASS = <span class="hljs-string">"mypassword"</span>

REDIS_HOST = <span class="hljs-string">"10.0.0.130"</span> <span class="hljs-comment"># IP of your redis-cache-lxc</span>

<span class="hljs-comment"># --- Rate Limiter Settings ---</span>
RATE_LIMIT_COUNT = <span class="hljs-number">10</span>  <span class="hljs-comment"># 10 requests</span>
RATE_LIMIT_WINDOW = <span class="hljs-number">60</span> <span class="hljs-comment"># per 60 seconds</span>

<span class="hljs-comment"># Establish a reusable Redis connection</span>
redis_client = redis.Redis(host=REDIS_HOST, port=<span class="hljs-number">6379</span>, decode_responses=<span class="hljs-literal">True</span>)

<span class="hljs-meta">@app.before_request</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">rate_limiter</span>():</span>
    <span class="hljs-comment"># Use the user's IP address as the key</span>
    <span class="hljs-comment"># In a real app, you'd handle proxies via request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)</span>
    key = <span class="hljs-string">f"rate_limit:<span class="hljs-subst">{request.remote_addr}</span>"</span>
    now = time.time()

    <span class="hljs-comment"># Use a Redis pipeline for atomic operations</span>
    pipe = redis_client.pipeline()
    <span class="hljs-comment"># 1. Add current request timestamp. The score and member are the same.</span>
    pipe.zadd(key, {str(now): now})
    <span class="hljs-comment"># 2. Remove all timestamps older than our window</span>
    pipe.zremrangebyscore(key, <span class="hljs-number">0</span>, now - RATE_LIMIT_WINDOW)
    <span class="hljs-comment"># 3. Get the count of remaining timestamps</span>
    pipe.zcard(key)
    <span class="hljs-comment"># 4. Set an expiration on the key so it cleans itself up</span>
    pipe.expire(key, RATE_LIMIT_WINDOW)

    <span class="hljs-comment"># Execute the pipeline and get the results</span>
    results = pipe.execute()
    request_count = results[<span class="hljs-number">2</span>] <span class="hljs-comment"># The result of the zcard command</span>

    <span class="hljs-keyword">if</span> request_count &gt; RATE_LIMIT_COUNT:
        <span class="hljs-comment"># Return a 429 Too Many Requests error</span>
        <span class="hljs-keyword">return</span> jsonify(error=<span class="hljs-string">"Rate limit exceeded"</span>), <span class="hljs-number">429</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_db_connection</span>():</span>
    conn = psycopg2.connect(host=DB_HOST, dbname=DB_NAME, user=DB_USER, password=DB_PASS)
    <span class="hljs-keyword">return</span> conn

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init_db</span>():</span>
    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute(<span class="hljs-string">'''
        CREATE TABLE IF NOT EXISTS urls (
            id SERIAL PRIMARY KEY,
            short_code VARCHAR(6) UNIQUE NOT NULL,
            original_url TEXT NOT NULL
        );
    '''</span>)
    <span class="hljs-comment"># Check if the index exists before creating it</span>
    cur.execute(<span class="hljs-string">'''
        SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
        WHERE c.relname = 'idx_original_url' AND n.nspname = 'public';
    '''</span>)
    <span class="hljs-keyword">if</span> cur.fetchone() <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        cur.execute(<span class="hljs-string">'CREATE INDEX idx_original_url ON urls (original_url);'</span>)
    conn.commit()
    cur.close()
    conn.close()

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_short_code</span>(<span class="hljs-params">length=<span class="hljs-number">6</span></span>):</span>
    chars = string.ascii_letters + string.digits
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>.join(random.choice(chars) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(length))

<span class="hljs-meta">@app.route("/", methods=['GET'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">index</span>():</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"URL Shortener is running!\n"</span>, <span class="hljs-number">200</span>

<span class="hljs-meta">@app.route('/shorten', methods=['POST'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">shorten_url</span>():</span>
    original_url = request.form[<span class="hljs-string">'url'</span>]
    conn = get_db_connection()
    cur = conn.cursor()

    cur.execute(<span class="hljs-string">"SELECT short_code FROM urls WHERE original_url = %s"</span>, (original_url,))
    existing_url = cur.fetchone()

    <span class="hljs-keyword">if</span> existing_url:
        short_code = existing_url[<span class="hljs-number">0</span>]
    <span class="hljs-keyword">else</span>:
        short_code = generate_short_code()
        cur.execute(<span class="hljs-string">"INSERT INTO urls (short_code, original_url) VALUES (%s, %s)"</span>, (short_code, original_url))
        conn.commit()

    cur.close()
    conn.close()

    <span class="hljs-keyword">return</span> jsonify(short_url=<span class="hljs-string">f"/<span class="hljs-subst">{short_code}</span>"</span>)

<span class="hljs-meta">@app.route('/&lt;short_code&gt;')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">redirect_to_url</span>(<span class="hljs-params">short_code</span>):</span>
    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute(<span class="hljs-string">"SELECT original_url FROM urls WHERE short_code = %s"</span>, (short_code,))
    url_record = cur.fetchone()
    cur.close()
    conn.close()

    <span class="hljs-keyword">if</span> url_record:
        <span class="hljs-keyword">return</span> redirect(url_record[<span class="hljs-number">0</span>])
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-string">"URL not found"</span>, <span class="hljs-number">404</span>

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    init_db() 
    app.run(host=<span class="hljs-string">'0.0.0.0'</span>, port=<span class="hljs-number">5000</span>)
</code></pre>
<h3 id="heading-how-it-works-step-by-step"><strong>How It Works, Step-by-Step</strong></h3>
<ol>
<li><p><strong>Identify the User:</strong> We create a unique Redis key for each user based on their IP address: <code>rate_limit:1.2.3.4</code>.</p>
</li>
<li><p><strong>Use a Pipeline:</strong> Network latency can be a bottleneck. A Redis pipeline bundles multiple commands into a single request-response cycle. This is much more efficient than sending them one by one. It also ensures the sequence of commands runs without being interrupted by commands from other clients.</p>
</li>
<li><p><strong>Log the Current Request (ZADD):</strong> We add the current timestamp (as a Unix epoch) to a sorted set. We use the timestamp for both the "member" and the "score," which allows us to easily filter by time.</p>
</li>
<li><p><strong>Clean Up Old Requests (ZREMRANGEBYSCORE):</strong> This is the "sliding window" part. We remove any timestamps from the set that are older than our <code>RATE_LIMIT_WINDOW</code> (60 seconds). This efficiently discards requests that are no longer relevant to the current rate limit period.</p>
</li>
<li><p><strong>Count the Recent Requests (ZCARD):</strong> We get the cardinality (the number of items) in the set. After the previous step, this number is our count of requests within the last 60 seconds.</p>
</li>
<li><p><strong>Mark the current record to expire (EXPIRE):</strong> We set an expiration on the key itself. If a user stops making requests, Redis will automatically delete their rate limit data after 60 seconds, preventing memory from filling up with old keys.</p>
</li>
<li><p><strong>Execute and Check:</strong> The <code>pipe.execute()</code> command sends all our bundled commands to Redis. We then check the result of our ZCARD command. If the count exceeds our <code>RATE_LIMIT_COUNT</code>, we immediately return a 429 error.</p>
</li>
</ol>
<p>This approach is incredibly fast and efficient. All the heavy lifting is done inside Redis, which is optimized for these kinds of operations.</p>
<h2 id="heading-step-3-containerizing-and-testing"><strong>Step 3: Containerizing and Testing</strong></h2>
<p>To deploy our application consistently across multiple VMs, we use Docker. Our Dockerfile is standard for a Python application: it starts from a Python image, installs dependencies from <code>requirements.txt</code>, copies the application code, and defines the command to run the app.</p>
<p>But how do we know it works? We test it!</p>
<p>We use <code>k6</code>, a modern load testing tool, to simulate heavy traffic. Our test script, <code>rate-test.js</code>, is designed specifically to verify the rate limiter.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> http <span class="hljs-keyword">from</span> <span class="hljs-string">'k6/http'</span>;
<span class="hljs-keyword">import</span> { check, sleep } <span class="hljs-keyword">from</span> <span class="hljs-string">'k6'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> options = {
  <span class="hljs-attr">stages</span>: [
    <span class="hljs-comment">// Ramp up to 20 users. This is more than the 10 req/min limit</span>
    <span class="hljs-comment">// and should trigger the rate limiter.</span>
    { <span class="hljs-attr">duration</span>: <span class="hljs-string">'30s'</span>, <span class="hljs-attr">target</span>: <span class="hljs-number">20</span> },
    { <span class="hljs-attr">duration</span>: <span class="hljs-string">'1m'</span>, <span class="hljs-attr">target</span>: <span class="hljs-number">20</span> },
    { <span class="hljs-attr">duration</span>: <span class="hljs-string">'10s'</span>, <span class="hljs-attr">target</span>: <span class="hljs-number">0</span> },
  ],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> url = <span class="hljs-string">'http://10.0.0.100/shorten'</span>; <span class="hljs-comment">// The Load Balancer IP</span>
  <span class="hljs-keyword">const</span> payload = { <span class="hljs-attr">url</span>: <span class="hljs-string">`https://www.test-ratelimit-<span class="hljs-subst">${<span class="hljs-built_in">Math</span>.random()}</span>.com`</span> };

  <span class="hljs-keyword">const</span> res = http.post(url, payload);

  <span class="hljs-comment">// Check if the request was successful OR if it was correctly rate-limited</span>
  check(res, {
    <span class="hljs-string">'status is 200 (OK)'</span>: <span class="hljs-function">(<span class="hljs-params">r</span>) =&gt;</span> r.status === <span class="hljs-number">200</span>,
    <span class="hljs-string">'status is 429 (Too Many Requests)'</span>: <span class="hljs-function">(<span class="hljs-params">r</span>) =&gt;</span> r.status === <span class="hljs-number">429</span>,
  });

  sleep(<span class="hljs-number">1</span>);
}
</code></pre>
<p>The stages array configures the test to gradually increase the number of virtual users to 20. Since our rate limit is 10 requests per minute, this load is guaranteed to trigger the limiter.</p>
<p>The <code>check</code> function is the crucial part. It verifies that the server's response code is either 200 (meaning the request was successful) or 429 (meaning our rate limiter correctly blocked the request).</p>
<p>We should see about 10 of our requests go through of the around 1600 requests per minute that we send from the same IP address.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758477504110/3a2f3f0f-8db0-453d-8900-42a6d0966a11.gif" alt="A gif showing the test run of the load testing script" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>We can also check the logs on our webserver to see all the requests that were sent to it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758477959201/80a39d07-1c4e-4d45-8a42-9ac2ce6f360d.gif" alt="A small gif demonstrating Web Server Logs" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And if we look at the Redis cache/database itself, we’ll see all the keys and the TTL at which they expire.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758478780827/6a07a2ee-0ad0-4b60-899f-d6a0453edbe7.png" alt="6a07a2ee-0ad0-4b60-899f-d6a0453edbe7" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>This is how we rate limit applications using a Redis Cache Server.</p>
<p>Here are the complete files used in the project.</p>
<pre><code class="lang-yaml">    <span class="hljs-string">terraform</span> {
    <span class="hljs-string">required_providers</span> {
        <span class="hljs-string">proxmox</span> <span class="hljs-string">=</span> {
        <span class="hljs-string">source</span>  <span class="hljs-string">=</span> <span class="hljs-string">"telmate/proxmox"</span>
        <span class="hljs-string">version</span> <span class="hljs-string">=</span> <span class="hljs-string">"3.0.2-rc04"</span>
        }
    }
    }

    <span class="hljs-string">provider</span> <span class="hljs-string">"proxmox"</span> {
    <span class="hljs-string">pm_api_url</span>          <span class="hljs-string">=</span> <span class="hljs-string">var.proxmox_api_url</span>
    <span class="hljs-string">pm_api_token_id</span>     <span class="hljs-string">=</span> <span class="hljs-string">var.proxmox_api_token_id</span>
    <span class="hljs-string">pm_api_token_secret</span> <span class="hljs-string">=</span> <span class="hljs-string">var.proxmox_api_token_secret</span>
    <span class="hljs-string">pm_tls_insecure</span>     <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
    }

    <span class="hljs-comment"># --- Shared Provisioner Connection Settings ---</span>
    <span class="hljs-string">locals</span> {
        <span class="hljs-string">connection_settings</span> <span class="hljs-string">=</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
        }
    }

    <span class="hljs-comment"># --- Database LXC Containers ---</span>
    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_lxc"</span> <span class="hljs-string">"postgres_db"</span> {
    <span class="hljs-string">hostname</span>     <span class="hljs-string">=</span> <span class="hljs-string">"postgres-db-lxc"</span>
    <span class="hljs-string">target_node</span>  <span class="hljs-string">=</span> <span class="hljs-string">var.target_node</span>
    <span class="hljs-string">ostemplate</span>   <span class="hljs-string">=</span> <span class="hljs-string">var.lxc_template</span>

    <span class="hljs-string">rootfs</span> {
        <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
        <span class="hljs-string">size</span> <span class="hljs-string">=</span> <span class="hljs-string">"8G"</span>
    }

    <span class="hljs-string">password</span>     <span class="hljs-string">=</span> <span class="hljs-string">"admin"</span>
    <span class="hljs-string">unprivileged</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
    <span class="hljs-string">start</span>        <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

    <span class="hljs-string">features</span> {
        <span class="hljs-string">nesting</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-comment"># keyctl = true</span>
    }

    <span class="hljs-string">network</span> {
        <span class="hljs-string">name</span>   <span class="hljs-string">=</span> <span class="hljs-string">"eth0"</span>
        <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
        <span class="hljs-string">ip</span>     <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.200/24"</span>
        <span class="hljs-string">gw</span>     <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.1"</span>
    }

    <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
        <span class="hljs-string">connection</span> {
        <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
        <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
        <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
        }
        <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
        <span class="hljs-string">"sudo apt-get update"</span>,
        <span class="hljs-string">"sudo apt-get install -y docker.io docker-compose python3-setuptools"</span>,
        <span class="hljs-string">"sudo usermod -aG docker ${var.ssh_user}"</span>,
        <span class="hljs-string">"sudo mkdir -p /opt/postgres"</span>,
        <span class="hljs-string">"sudo chown ${var.ssh_user}:${var.ssh_user} /opt/postgres"</span>
        ]
    }

    <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
        <span class="hljs-string">connection</span> {
        <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
        <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
        <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
        }
        <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../databases/pg-docker-compose.yml"</span>
        <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/opt/postgres/docker-compose.yml"</span>
    }

    <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
        <span class="hljs-string">inline</span>     <span class="hljs-string">=</span> [<span class="hljs-string">"cd /opt/postgres &amp;&amp; sudo docker-compose up -d"</span>]

        <span class="hljs-string">connection</span> {
        <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
        <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
        <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
        }
    }
    }

    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_lxc"</span> <span class="hljs-string">"mongo_db"</span> {
        <span class="hljs-string">hostname</span>    <span class="hljs-string">=</span> <span class="hljs-string">"mongo-db-lxc"</span>
        <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">var.target_node</span>
        <span class="hljs-string">ostemplate</span>  <span class="hljs-string">=</span> <span class="hljs-string">var.lxc_template</span>

        <span class="hljs-string">rootfs</span> {
            <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
            <span class="hljs-string">size</span> <span class="hljs-string">=</span> <span class="hljs-string">"8G"</span>
        }

        <span class="hljs-string">password</span>    <span class="hljs-string">=</span> <span class="hljs-string">"admin"</span>
        <span class="hljs-string">unprivileged</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">start</span>       <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

        <span class="hljs-string">features</span> {
            <span class="hljs-string">nesting</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-comment"># keyctl = true # Somehow this is blocking the apply command</span>
        }

        <span class="hljs-string">network</span> {
            <span class="hljs-string">name</span>   <span class="hljs-string">=</span> <span class="hljs-string">"eth0"</span>
            <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
            <span class="hljs-string">ip</span>     <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.210/24"</span>
            <span class="hljs-string">gw</span>     <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.1"</span>
        }

        <span class="hljs-comment"># Provisioners similar to postgres_db</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">connection</span> {
                <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
                <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
                <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
                <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
            }
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
            <span class="hljs-string">"sudo apt-get update"</span>,
            <span class="hljs-string">"sudo apt-get install -y docker.io docker-compose python3-setuptools"</span>,
            <span class="hljs-string">"sudo usermod -aG docker ${var.ssh_user}"</span>,
            <span class="hljs-string">"sudo mkdir -p /opt/mongo"</span>,
            <span class="hljs-string">"sudo chown ${var.ssh_user}:${var.ssh_user} /opt/mongo"</span>
            ]
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
            }
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../databases/mongo-docker-compose.yml"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/opt/mongo/docker-compose.yml"</span>
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">split("/"</span>, <span class="hljs-string">self.network</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.ip)</span>[<span class="hljs-number">0</span>]
            }
            <span class="hljs-string">inline</span>     <span class="hljs-string">=</span> [<span class="hljs-string">"cd /opt/mongo &amp;&amp; docker-compose up -d"</span>]
        }
    }

    <span class="hljs-comment"># --- Redis Cache for Rate Limiter ---</span>
    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_vm_qemu"</span> <span class="hljs-string">"redis_cache"</span> {

        <span class="hljs-string">vmid</span>        <span class="hljs-string">=</span> <span class="hljs-number">130</span>
        <span class="hljs-string">name</span>        <span class="hljs-string">=</span> <span class="hljs-string">"redis-cache-rate-limiter"</span>
        <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">"pve"</span>
        <span class="hljs-string">agent</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        <span class="hljs-string">cpu</span> {
            <span class="hljs-string">cores</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        }

        <span class="hljs-string">memory</span>      <span class="hljs-string">=</span> <span class="hljs-number">1024</span>
        <span class="hljs-string">boot</span>        <span class="hljs-string">=</span> <span class="hljs-string">"order=scsi0"</span> <span class="hljs-comment"># has to be the same as the OS disk of the template</span>
        <span class="hljs-string">clone</span>       <span class="hljs-string">=</span> <span class="hljs-string">"debian12-cloudinit"</span> <span class="hljs-comment"># The name of the template</span>
        <span class="hljs-string">scsihw</span>      <span class="hljs-string">=</span> <span class="hljs-string">"virtio-scsi-single"</span>
        <span class="hljs-string">vm_state</span>    <span class="hljs-string">=</span> <span class="hljs-string">"running"</span>
        <span class="hljs-string">automatic_reboot</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

        <span class="hljs-comment"># Cloud-Init configuration</span>
        <span class="hljs-string">cicustom</span>   <span class="hljs-string">=</span> <span class="hljs-string">"vendor=local:snippets/qemu-guest-agent.yml"</span> <span class="hljs-comment"># /var/lib/vz/snippets/qemu-guest-agent.yml</span>
        <span class="hljs-string">ciupgrade</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">nameserver</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.1.1.1 8.8.8.8"</span>
        <span class="hljs-string">ipconfig0</span>  <span class="hljs-string">=</span> <span class="hljs-string">"ip=10.0.0.130/24,gw=10.0.0.1"</span>
        <span class="hljs-string">skip_ipv6</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">ciuser</span>     <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">cipassword</span> <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_password</span>
        <span class="hljs-string">sshkeys</span>    <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_key</span>

        <span class="hljs-comment"># Most cloud-init images require a serial device for their display</span>
        <span class="hljs-string">serial</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
        }

        <span class="hljs-string">disks</span> {
            <span class="hljs-string">scsi</span> {
            <span class="hljs-string">scsi0</span> {
                <span class="hljs-comment"># We have to specify the disk from our template, else Terraform will think it's not supposed to be there</span>
                <span class="hljs-string">disk</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                <span class="hljs-comment"># The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated</span>
                <span class="hljs-string">size</span>    <span class="hljs-string">=</span> <span class="hljs-string">"5G"</span> 
                }
            }
            }
            <span class="hljs-string">ide</span> {
            <span class="hljs-comment"># Some images require a cloud-init disk on the IDE controller, others on the SCSI or SATA controller</span>
            <span class="hljs-string">ide1</span> {
                <span class="hljs-string">cloudinit</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                }
            }
            }
        }

        <span class="hljs-string">network</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
            <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
            <span class="hljs-string">model</span>  <span class="hljs-string">=</span> <span class="hljs-string">"virtio"</span>
        }

        <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.130"</span>
        }

        <span class="hljs-comment"># 1. Install Docker and create the final app directory</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-comment"># Wait for cloud-init to finish before doing anything else</span>
                <span class="hljs-string">"echo 'Waiting for cloud-init to finish...'"</span>,
                <span class="hljs-string">"while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Still waiting...' &amp;&amp; sleep 1; done"</span>,
                <span class="hljs-string">"echo 'Cloud-init finished.'"</span>,

                <span class="hljs-comment"># Now, safely install packages</span>
                <span class="hljs-string">"sudo apt-get update -y"</span>,
                <span class="hljs-string">"sudo apt-get install -y docker.io docker-compose"</span>,
                <span class="hljs-string">"sudo mkdir -p /opt/redis"</span>,
            ]
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../caching/redis-docker-compose.yml"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/docker-compose.yml"</span>
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [ <span class="hljs-string">"sudo mv /home/${var.ssh_user}/docker-compose.yml /opt/redis/docker-compose.yml"</span> ]
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [ <span class="hljs-string">"cd /opt/redis &amp;&amp; sudo docker-compose up -d"</span> ]
        }
    }

    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_vm_qemu"</span> <span class="hljs-string">"web-servers"</span> {

        <span class="hljs-string">count</span> <span class="hljs-string">=</span> <span class="hljs-number">2</span>

        <span class="hljs-string">vmid</span>        <span class="hljs-string">=</span> <span class="hljs-string">count.index</span> <span class="hljs-string">+</span> <span class="hljs-number">150</span>
        <span class="hljs-string">name</span>        <span class="hljs-string">=</span> <span class="hljs-string">"web-server-tf-${count.index + 1}"</span>
        <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">"pve"</span>
        <span class="hljs-string">agent</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        <span class="hljs-string">cpu</span> {
            <span class="hljs-string">cores</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        }
        <span class="hljs-string">memory</span>      <span class="hljs-string">=</span> <span class="hljs-number">1024</span>
        <span class="hljs-string">boot</span>        <span class="hljs-string">=</span> <span class="hljs-string">"order=scsi0"</span> <span class="hljs-comment"># has to be the same as the OS disk of the template</span>
        <span class="hljs-string">clone</span>       <span class="hljs-string">=</span> <span class="hljs-string">"debian12-cloudinit"</span> <span class="hljs-comment"># The name of the template</span>
        <span class="hljs-string">scsihw</span>      <span class="hljs-string">=</span> <span class="hljs-string">"virtio-scsi-single"</span>
        <span class="hljs-string">vm_state</span>    <span class="hljs-string">=</span> <span class="hljs-string">"running"</span>
        <span class="hljs-string">automatic_reboot</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

        <span class="hljs-comment"># Cloud-Init configuration</span>
        <span class="hljs-string">cicustom</span>   <span class="hljs-string">=</span> <span class="hljs-string">"vendor=local:snippets/qemu-guest-agent.yml"</span> <span class="hljs-comment"># /var/lib/vz/snippets/qemu-guest-agent.yml</span>
        <span class="hljs-string">ciupgrade</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">nameserver</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.1.1.1 8.8.8.8"</span>
        <span class="hljs-string">ipconfig0</span>  <span class="hljs-string">=</span> <span class="hljs-string">"ip=10.0.0.${111 + count.index}/24,gw=10.0.0.1"</span>
        <span class="hljs-string">skip_ipv6</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">ciuser</span>     <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">cipassword</span> <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_password</span>
        <span class="hljs-string">sshkeys</span>    <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_key</span>

        <span class="hljs-comment"># Most cloud-init images require a serial device for their display</span>
        <span class="hljs-string">serial</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
        }

        <span class="hljs-string">disks</span> {
            <span class="hljs-string">scsi</span> {
            <span class="hljs-string">scsi0</span> {
                <span class="hljs-comment"># We have to specify the disk from our template, else Terraform will think it's not supposed to be there</span>
                <span class="hljs-string">disk</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                <span class="hljs-comment"># The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated</span>
                <span class="hljs-string">size</span>    <span class="hljs-string">=</span> <span class="hljs-string">"5G"</span> 
                }
            }
            }
            <span class="hljs-string">ide</span> {
            <span class="hljs-comment"># Some images require a cloud-init disk on the IDE controller, others on the SCSI or SATA controller</span>
            <span class="hljs-string">ide1</span> {
                <span class="hljs-string">cloudinit</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                }
            }
            }
        }

        <span class="hljs-string">network</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
            <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
            <span class="hljs-string">model</span>  <span class="hljs-string">=</span> <span class="hljs-string">"virtio"</span>
        }

        <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.${111 + count.index}"</span>
        }

        <span class="hljs-comment"># 1. Install Docker and create the final app directory</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-comment"># Wait for cloud-init to finish before doing anything else</span>
                <span class="hljs-string">"echo 'Waiting for cloud-init to finish...'"</span>,
                <span class="hljs-string">"while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Still waiting...' &amp;&amp; sleep 1; done"</span>,
                <span class="hljs-string">"echo 'Cloud-init finished.'"</span>,

                <span class="hljs-comment"># Now, safely install packages</span>
                <span class="hljs-string">"sudo apt-get update -y"</span>,
                <span class="hljs-string">"sudo apt-get install -y docker.io"</span>,
                <span class="hljs-string">"sudo mkdir -p /opt/app"</span>,
            ]
        }

        <span class="hljs-comment"># 2. Upload ONLY the necessary files to the user's home directory</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../web-servers/app.py"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/app.py"</span>
        }
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../web-servers/Dockerfile"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/Dockerfile"</span>
        }
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../web-servers/requirements.txt"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/requirements.txt"</span>
        }

        <span class="hljs-comment"># 4. Move files from the home directory, build the image, and run the container</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-comment"># Move each file individually to be compatible with all shells</span>
                <span class="hljs-string">"sudo mv /home/${var.ssh_user}/app.py /opt/app/"</span>,
                <span class="hljs-string">"sudo mv /home/${var.ssh_user}/Dockerfile /opt/app/"</span>,
                <span class="hljs-string">"sudo mv /home/${var.ssh_user}/requirements.txt /opt/app/"</span>,

                <span class="hljs-comment"># Build the Docker image</span>
                <span class="hljs-string">"sudo docker build -t my-python-app /opt/app"</span>,

                <span class="hljs-comment"># Stop and remove any old containers to prevent conflicts</span>
                <span class="hljs-string">"sudo docker stop $(sudo docker ps -q --filter ancestor=my-python-app) 2&gt;/dev/null || true"</span>,
                <span class="hljs-string">"sudo docker rm $(sudo docker ps -aq --filter ancestor=my-python-app) 2&gt;/dev/null || true"</span>,

                <span class="hljs-comment"># Run the new container</span>
                <span class="hljs-string">"sudo docker run -d --restart always -p 80:5000 my-python-app"</span>
            ]
        }

        <span class="hljs-comment"># In your proxmox_vm_qemu "web_servers" resource</span>
        <span class="hljs-string">depends_on</span> <span class="hljs-string">=</span> [
            <span class="hljs-string">proxmox_lxc.postgres_db</span>,
            <span class="hljs-string">proxmox_vm_qemu.redis_cache</span>
        ]
    }

    <span class="hljs-comment"># --- Load Balancer VM ---</span>
    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_vm_qemu"</span> <span class="hljs-string">"load_balancer"</span> {
        <span class="hljs-string">name</span>        <span class="hljs-string">=</span> <span class="hljs-string">"lb-1"</span>
        <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">var.target_node</span>
        <span class="hljs-string">clone</span>       <span class="hljs-string">=</span> <span class="hljs-string">var.vm_template</span>
        <span class="hljs-string">agent</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        <span class="hljs-string">cpu</span> {
            <span class="hljs-string">cores</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        }
        <span class="hljs-string">memory</span>      <span class="hljs-string">=</span> <span class="hljs-number">512</span>
        <span class="hljs-string">boot</span>        <span class="hljs-string">=</span> <span class="hljs-string">"order=scsi0"</span> <span class="hljs-comment"># has to be the same as the OS disk of the template</span>
        <span class="hljs-string">scsihw</span>      <span class="hljs-string">=</span> <span class="hljs-string">"virtio-scsi-single"</span>
        <span class="hljs-string">vm_state</span>    <span class="hljs-string">=</span> <span class="hljs-string">"running"</span>
        <span class="hljs-string">automatic_reboot</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

        <span class="hljs-comment"># --- Add these lines for Cloud Init Drive ---</span>
                <span class="hljs-comment"># --- Add these lines for Cloud Init Drive ---</span>
        <span class="hljs-string">cicustom</span>   <span class="hljs-string">=</span> <span class="hljs-string">"vendor=local:snippets/qemu-guest-agent.yml"</span> <span class="hljs-comment"># /var/lib/vz/snippets/qemu-guest-agent.yml</span>
        <span class="hljs-string">ciupgrade</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">nameserver</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.1.1.1 8.8.8.8"</span>
        <span class="hljs-string">ipconfig0</span>  <span class="hljs-string">=</span> <span class="hljs-string">"ip=10.0.0.100/24,gw=10.0.0.1"</span>
        <span class="hljs-string">skip_ipv6</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">ciuser</span>     <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">cipassword</span> <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_password</span>
        <span class="hljs-string">sshkeys</span>    <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_key</span>

        <span class="hljs-comment"># Most cloud-init images require a serial device for their display</span>
        <span class="hljs-string">serial</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
        }

        <span class="hljs-string">disks</span> {
            <span class="hljs-string">scsi</span> {
            <span class="hljs-string">scsi0</span> {
                <span class="hljs-comment"># We have to specify the disk from our template, else Terraform will think it's not supposed to be there</span>
                <span class="hljs-string">disk</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                <span class="hljs-comment"># The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated</span>
                <span class="hljs-string">size</span>    <span class="hljs-string">=</span> <span class="hljs-string">"5G"</span> 
                }
            }
            }
            <span class="hljs-string">ide</span> {
            <span class="hljs-comment"># Some images require a cloud-init disk on the IDE controller, others on the SCSI or SATA controller</span>
            <span class="hljs-string">ide1</span> {
                <span class="hljs-string">cloudinit</span> {
                <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                }
            }
            }
        }

        <span class="hljs-string">network</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
            <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
            <span class="hljs-string">model</span>  <span class="hljs-string">=</span> <span class="hljs-string">"virtio"</span>
        }

        <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.100"</span>
        }

        <span class="hljs-comment"># Step 1: Install Nginx</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-comment"># Wait for cloud-init to finish before doing anything else</span>
                <span class="hljs-string">"echo 'Waiting for cloud-init to finish...'"</span>,
                <span class="hljs-string">"while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Still waiting...' &amp;&amp; sleep 1; done"</span>,
                <span class="hljs-string">"echo 'Cloud-init finished.'"</span>,

                <span class="hljs-comment"># Now, safely install packages</span>
                <span class="hljs-string">"sudo apt-get update -y"</span>,
                <span class="hljs-string">"sudo apt-get install -y nginx"</span>
            ]
        }

        <span class="hljs-comment"># Step 2: Upload config to a temporary location</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../web-servers/nginx.conf"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/tmp/nginx.conf"</span> <span class="hljs-comment"># Use /tmp instead</span>
        }

        <span class="hljs-comment"># Step 3: Use sudo to move the file to its final destination and reload nginx</span>
        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-string">"sudo mv /tmp/nginx.conf /etc/nginx/sites-available/default"</span>,
                <span class="hljs-string">"sudo systemctl reload nginx"</span>
            ]
        }
    }


    <span class="hljs-comment"># --- Load Tester VM ---</span>
    <span class="hljs-string">resource</span> <span class="hljs-string">"proxmox_vm_qemu"</span> <span class="hljs-string">"load_tester"</span> {
        <span class="hljs-string">name</span>        <span class="hljs-string">=</span> <span class="hljs-string">"load-tester-vm"</span>
        <span class="hljs-string">target_node</span> <span class="hljs-string">=</span> <span class="hljs-string">var.target_node</span>
        <span class="hljs-string">clone</span>       <span class="hljs-string">=</span> <span class="hljs-string">var.vm_template</span>
        <span class="hljs-string">agent</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        <span class="hljs-string">cpu</span> {
            <span class="hljs-string">cores</span>       <span class="hljs-string">=</span> <span class="hljs-number">1</span>
        }
        <span class="hljs-string">memory</span>      <span class="hljs-string">=</span> <span class="hljs-number">1024</span>
        <span class="hljs-string">boot</span>        <span class="hljs-string">=</span> <span class="hljs-string">"order=scsi0"</span> <span class="hljs-comment"># has to be the same as the OS disk of the template</span>
        <span class="hljs-string">scsihw</span>      <span class="hljs-string">=</span> <span class="hljs-string">"virtio-scsi-single"</span>
        <span class="hljs-string">vm_state</span>    <span class="hljs-string">=</span> <span class="hljs-string">"running"</span>
        <span class="hljs-string">automatic_reboot</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

        <span class="hljs-comment"># --- Add these lines for Cloud Init Drive ---</span>
        <span class="hljs-string">cicustom</span>   <span class="hljs-string">=</span> <span class="hljs-string">"vendor=local:snippets/qemu-guest-agent.yml"</span> <span class="hljs-comment"># /var/lib/vz/snippets/qemu-guest-agent.yml</span>
        <span class="hljs-string">ciupgrade</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">nameserver</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.1.1.1 8.8.8.8"</span>
        <span class="hljs-string">ipconfig0</span>  <span class="hljs-string">=</span> <span class="hljs-string">"ip=10.0.0.160/24,gw=10.0.0.1"</span>
        <span class="hljs-string">skip_ipv6</span>  <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
        <span class="hljs-string">ciuser</span>     <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
        <span class="hljs-string">cipassword</span> <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_password</span>
        <span class="hljs-string">sshkeys</span>    <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_key</span>

        <span class="hljs-comment"># Most cloud-init images require a serial device for their display</span>
        <span class="hljs-string">serial</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
        }

        <span class="hljs-string">disks</span> {
            <span class="hljs-string">scsi</span> {
                <span class="hljs-string">scsi0</span> {
                    <span class="hljs-comment"># We have to specify the disk from our template, else Terraform will think it's not supposed to be there</span>
                    <span class="hljs-string">disk</span> {
                    <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                    <span class="hljs-comment"># The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated</span>
                    <span class="hljs-string">size</span>    <span class="hljs-string">=</span> <span class="hljs-string">"5G"</span> 
                    }
                }
            }

            <span class="hljs-string">ide</span> {
            <span class="hljs-comment"># Some images require a cloud-init disk on the IDE controller, others on the SCSI or SATA controller</span>
                <span class="hljs-string">ide1</span> {
                    <span class="hljs-string">cloudinit</span> {
                    <span class="hljs-string">storage</span> <span class="hljs-string">=</span> <span class="hljs-string">"local-lvm"</span>
                    }
                }
            }
        }

        <span class="hljs-string">network</span> {
            <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>
            <span class="hljs-string">bridge</span> <span class="hljs-string">=</span> <span class="hljs-string">"vmbr0"</span>
            <span class="hljs-string">model</span>  <span class="hljs-string">=</span> <span class="hljs-string">"virtio"</span>
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"remote-exec"</span> {
            <span class="hljs-string">connection</span> {
                <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
                <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
                <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
                <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.160"</span>
            }
            <span class="hljs-string">inline</span> <span class="hljs-string">=</span> [
                <span class="hljs-comment"># Wait for cloud-init to finish</span>
                <span class="hljs-string">"echo 'Waiting for cloud-init to finish...'"</span>,
                <span class="hljs-string">"while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Still waiting...' &amp;&amp; sleep 1; done"</span>,
                <span class="hljs-string">"echo 'Cloud-init finished.'"</span>,

                <span class="hljs-comment"># Install prerequisites</span>
                <span class="hljs-string">"sudo apt-get update -y"</span>,
                <span class="hljs-string">"sudo apt-get install -y gnupg curl"</span>,

                <span class="hljs-comment"># Add the k6 repository and key</span>
                <span class="hljs-string">"curl -sL https://dl.k6.io/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/k6-archive-keyring.gpg"</span>,
                <span class="hljs-string">"echo 'deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main' | sudo tee /etc/apt/sources.list.d/k6.list"</span>,

                <span class="hljs-comment"># Install k6</span>
                <span class="hljs-string">"sudo apt-get update"</span>,
                <span class="hljs-string">"sudo apt-get install -y k6"</span>
            ]
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.160"</span>
            }
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../load-testing/script.js"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/script.js"</span>
        }

        <span class="hljs-string">provisioner</span> <span class="hljs-string">"file"</span> {
            <span class="hljs-string">connection</span> {
            <span class="hljs-string">type</span>        <span class="hljs-string">=</span> <span class="hljs-string">"ssh"</span>
            <span class="hljs-string">user</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.ssh_user</span>
            <span class="hljs-string">private_key</span> <span class="hljs-string">=</span> <span class="hljs-string">file(var.ssh_private_key_path)</span>
            <span class="hljs-string">host</span>        <span class="hljs-string">=</span> <span class="hljs-string">"10.0.0.160"</span>
            }
            <span class="hljs-string">source</span>      <span class="hljs-string">=</span> <span class="hljs-string">"../load-testing/rate-test.js"</span>
            <span class="hljs-string">destination</span> <span class="hljs-string">=</span> <span class="hljs-string">"/home/${var.ssh_user}/rate-test.js"</span>
        }

    }
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>You've now seen how to build a complete, scalable, and resilient system that includes a crucial component for modern web applications: a distributed rate limiter.</p>
<p>We've covered the entire stack:</p>
<ul>
<li><p><strong>Infrastructure as Code</strong> with Terraform to define our virtual machines. (check out my repo <a target="_blank" href="https://github.com/sravankaruturi/system-design">here</a> for all the code and any updates I make).</p>
</li>
<li><p>A <strong>centralized, high-speed cache</strong> with Redis to store our rate limiting data.</p>
</li>
<li><p>An efficient <strong>Sliding Window Log algorithm</strong> implemented in Python with Flask.</p>
</li>
<li><p><strong>Containerization</strong> with Docker for consistent deployment.</p>
</li>
<li><p><strong>Load balancing</strong> with Nginx to distribute traffic.</p>
</li>
<li><p><strong>Load testing</strong> with k6 to validate our implementation.</p>
</li>
</ul>
<p>If you’d like to learn more of the concepts that are used when building large scale systems please follow me at <a class="user-mention" href="https://hashnode.com/@sravankaruturi">Sravan Karuturi</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Robust Networking Layers in Swift with OpenAPI ]]>
                </title>
                <description>
                    <![CDATA[ What is the Problem We’re Solving? For many app developers, including me, writing the networking layer of an application is a familiar and tedious process. You write and test your first call and after that, it involves a repetitive cycle of tasks. Th... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-robust-networking-layers-in-swift-with-openapi/</link>
                <guid isPermaLink="false">687fd1a603524e3b4e7b77fe</guid>
                
                    <category>
                        <![CDATA[ OpenApi ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ openapi generator ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenAPI Specification ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SwiftUI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sravan Karuturi ]]>
                </dc:creator>
                <pubDate>Tue, 22 Jul 2025 18:00:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753206547489/dce9a849-1ccd-4cb0-bca8-f879a5aadf5f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="heading-what-is-the-problem-were-solving"><strong>What is the Problem We’re Solving?</strong></h2>
<p>For many app developers, including me, writing the networking layer of an application is a familiar and tedious process. You write and test your first call and after that, it involves a repetitive cycle of tasks.</p>
<p>This is how it would look in the case of Swift:</p>
<ol>
<li><p>You create a <code>URLSession</code> Instance.</p>
</li>
<li><p>You create a <code>URLRequest</code> Object.</p>
</li>
<li><p>You create the <code>@Codable</code> models to match the expected input and output from the server.</p>
</li>
</ol>
<p>You do the above steps for each API endpoint you have on your backend that your app uses. Not only is this process time-consuming and not challenging for developers, it’s also error prone. </p>
<p>In the above case, if there was a minor change in the backend API – perhaps a renamed field or a new property – this would lead to the app potentially breaking. But you wouldn’t know this until you shipped it to QA or in a worse case, your consumer. This is where the OpenAPI Specification emerges as a modern, robust solution. </p>
<p>In this tutorial, you’ll learn what OpenAPI is and how it can help make your development process better. After that, we’ll implement OpenAPI by creating a small SwiftUI app and using OpenAPI methodologies to interface with the <code>JSONPlaceholder</code> API. Let’s get started.</p>
<h2 id="heading-who-is-this-guide-for"><strong>Who is This Guide For?</strong></h2>
<p>This guide is intended both for new developers looking for best practices and for experienced developers looking to implement or learn more about the OpenAPI Specification. Let’s get into it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-openapi-and-why-should-you-care">What is OpenAPI and Why Should You Care?</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-benefits-for-swift-ios-developers">Benefits for Swift (iOS) Developers</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-a-practical-guide-to-implementing-this-solution">A Practical Guide to Implementing This Solution</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-create-a-good-openapiyaml-file-the-specification">Step 1: Create a good openapi.yaml file (the specification)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-set-up-your-project">Step 2: Set up your project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-write-a-wrapper">Step 3: Write a wrapper</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-call-the-wrapper-and-display-the-data">Step 4: Call the wrapper and display the data</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-potential-pitfalls">Potential Pitfalls</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-verbose-or-ugly-generated-code">Verbose or ugly generated code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-large-specs-and-performance-issues">Large Specs and Performance Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-unsupported-spec-features">Unsupported Spec Features</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion-embrace-spec-driven-development">Conclusion: Embrace Spec-Driven Development</a></p>
</li>
</ul>
<h2 id="heading-what-is-openapi-and-why-should-you-care"><strong>What is OpenAPI and Why Should You Care?</strong></h2>
<p>At its core, the OpenAPI Specification provides a <em>standard, language-agnostic interface</em> for describing RESTful APIs. This specification, once populated, allows both humans and computers to discover and understand the capabilities of a service without needing to access the source code or the network requests.</p>
<p>The power of OpenAPI is that it acts as a <em>formal contract between different parts of the system.</em> This contract helps both frontend and backend programmers by removing ambiguity during design process. This also has added benefit of using code generators to generate boiler-plate code both on backend and on the client ( which we will also discuss today ).</p>
<p>Traditionally when you want to create a new API in a team, either the PM, the frontend engineer, or the backend engineer takes it upon themselves to request it. Then the backend team builds it and documents it. This in turn is used by the front end team to use the API. </p>
<p><code>Some Requester → Backend Team → Documentation → Frontend Team</code></p>
<p>If you’re using OpenAPI, when someone makes a request for a new API, it is formalized into a specification after deliberations with both the frontend and the backend team. This then serves as the source of truth and is used to generate the backend and the frontend code without as much interdependence.</p>
<p><code>Some Requester → All Teams → Specification → All Teams.</code></p>
<p>This not only streamlines the process of adding new APIs, but provides a definitive source of truth for each endpoint. This also makes it so that frontend engineers and backend engineers are not misaligned about a provided parameter in the result being an <code>Int</code> or a <code>String</code> and so on. <strong>It’s all in the Spec.</strong></p>
<h3 id="heading-benefits-for-swift-ios-developers"><strong>Benefits for Swift (iOS) Developers</strong></h3>
<p>Adopting OpenAPI and <code>swift-openapi-generator</code> brings a host of tangible benefits to the Swift/App development process. It transforms how applications interact with web services in a few key ways.</p>
<h4 id="heading-reduced-development-time-and-cost">Reduced Development Time and Cost</h4>
<p>The most immediate improvement you will see is the significant reduction in boilerplate code you have to write. The generator automates the creation of what is called boilerplate code or ceremonial code. This is the repetitive logic for network requests, response handling, and data model definitions.</p>
<p>By delegating this work, developers can work on the core features of the application which leads to faster and more interesting development cycles.</p>
<h4 id="heading-compile-time-type-safety">Compile Time Type Safety</h4>
<p>This has been a major improvement for me personally. Instead of relying on the “strongly” typed keys for JSON parsing, we now work with strongly typed models. The generator creates native Swift struct and enum types directly from the schemas defined in the OpenAPI document. This brings the power of a strongly-typed system to the networking and parsing layer.</p>
<p>For example, if the return value of an API is made optional, instead of crashing at runtime, we will fail to compile at build time. This forces us to address this issue right away. </p>
<h4 id="heading-improved-collaboration-and-interoperability">Improved Collaboration and Interoperability</h4>
<p>This makes sure that all the developers are on the same page with regard to a given endpoint. And since this specification is language agnostic, it will serve as a universal language for all teams involved in the project – mobile, web and backend. </p>
<h4 id="heading-other-tooling">Other Tooling</h4>
<p>Once you have a specification, you can use that to power a wide variety of tools. You can generate interactive documentation, create mock servers for frontend development, and run automated tests. </p>
<p>Alright hopefully you’re sold – so now how do you implement this into your project?</p>
<h2 id="heading-a-practical-guide-to-implementing-this-solution">A Practical Guide to Implementing This Solution</h2>
<p>We’ll now take a look at a practical example so you can understand how you can implement this in a project. This involves:</p>
<ul>
<li><p>Creating an openapi.yaml file to describe the API specification.</p>
</li>
<li><p>Configuring and integrating <code>swift-openapi-generator</code> into a SwiftUI application. </p>
</li>
<li><p>Prototyping an app that fetches and displays a list of posts from the <a target="_blank" href="https://jsonplaceholder.typicode.com/">https://jsonplaceholder.typicode.com/</a> </p>
</li>
</ul>
<p>To follow along, you will need Xcode installed and a basic understanding of Swift programming and SwiftUI for App development.</p>
<h3 id="heading-step-1-create-a-good-openapiyaml-file-the-specification">Step 1: Create a good openapi.yaml file (the specification)</h3>
<p>The quality of a specification is really important because it directly determines the quality of the code produced by <code>swift-openapi-generator</code>. If you don’t have a good specification, you might run into several issues that developers often complain about, like confusing and long method names.</p>
<p>For example, it might generate something like <code>get_all_my_meal_recipes_hyphen_detailed</code>. This might happen because the generator is forced to create a new name based on the API path if the identifier is not provided in the spec. So, instead of dealing with these issues one after the other, we will create a <em>good clear specification</em> to start with.</p>
<p>Since we’re using the <code>jsonplaceholder</code> as our backend server, we are limited by what tweaks we can make – but it is a fantastic project that lets us mimic a backend server.</p>
<p>In general, an OpenAPI.yaml file contains:</p>
<ol>
<li><p>OpenAPI Info and servers – This will provide the metadata about the API like the OpenAPI version, which server to point to for calls, and so on. </p>
</li>
<li><p>Paths – This will provide the available endpoints. In our case, it can contain /posts as one of them. We also will have to mention the kind of endpoint (get, post, put, and so on)</p>
</li>
<li><p>OperationID – This field instructs the generator to create a clear method with this name. </p>
</li>
<li><p>Responses – This defines the possible outcomes of an API call. We will specify the structure of a successful 200 OK response or any other errors here. </p>
</li>
<li><p>Components / Schemas – This defines all the reusable components and data models. If we have a Post schema definer here, the generator will use this to create a Post struct in Swift to match this. </p>
</li>
</ol>
<p>Keeping in mind all these elements, I compiled a yaml file for us to use for this tutorial:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># openapi.yaml</span>
<span class="hljs-attr">openapi:</span> <span class="hljs-string">"3.0.3"</span>
<span class="hljs-attr">info:</span>
  <span class="hljs-attr">title:</span> <span class="hljs-string">"JSONPlaceholder API"</span>
  <span class="hljs-attr">version:</span> <span class="hljs-string">"1.0.0"</span>
<span class="hljs-attr">servers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">url:</span> <span class="hljs-string">"https://jsonplaceholder.typicode.com"</span>
<span class="hljs-attr">paths:</span>
  <span class="hljs-string">/posts:</span>
    <span class="hljs-attr">get:</span>
      <span class="hljs-attr">summary:</span> <span class="hljs-string">"Get all posts"</span>
      <span class="hljs-attr">operationId:</span> <span class="hljs-string">"getPosts"</span>
      <span class="hljs-attr">responses:</span>
        <span class="hljs-attr">"200":</span>
          <span class="hljs-attr">description:</span> <span class="hljs-string">"A list of posts"</span>
          <span class="hljs-attr">content:</span>
            <span class="hljs-attr">application/json:</span>
              <span class="hljs-attr">schema:</span>
                <span class="hljs-attr">type:</span> <span class="hljs-string">array</span>
                <span class="hljs-attr">items:</span>
                  <span class="hljs-string">$ref:</span> <span class="hljs-string">"#/components/schemas/Post"</span>
<span class="hljs-attr">components:</span>
  <span class="hljs-attr">schemas:</span>
    <span class="hljs-attr">Post:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">object</span>
      <span class="hljs-attr">required:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">userId</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">id</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">title</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">body</span>
      <span class="hljs-attr">properties:</span>
        <span class="hljs-attr">userId:</span>
          <span class="hljs-attr">type:</span> <span class="hljs-string">integer</span>
        <span class="hljs-attr">id:</span>
          <span class="hljs-attr">type:</span> <span class="hljs-string">integer</span>
        <span class="hljs-attr">title:</span>
          <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
        <span class="hljs-attr">body:</span>
          <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
</code></pre>
<p>The first line here, <code>openapi: “3.0.3”</code>, just tells the generators and parsers that we are using version <code>3.0.3</code>. </p>
<p>Next, we have some more metadata – the name and version of the API. We also have the server we are calling with our APIs.</p>
<p>After defining this metadata, we now define our endpoints. For the sake of this example, let’s assume that we only have one endpoint to call to get posts. We represent this by saying <code>/posts</code> under paths. We then specify which kind it is by specifying <code>get:</code>.</p>
<p>We give a short description of what it does in the <code>summary</code> and then specify an <code>operationId</code> which is what we this function will be called in our generated code. We also specify exactly what structure the response will have, that is, a JSON of an array of <code>Posts</code>.</p>
<p>We then list any components we have across our APIs like the <code>Post</code>. Note that we are using the <code>Post</code> schema in the return response structure before we define it further down. The schemas in components will determine the Model structs we will generate using this yaml file.</p>
<h3 id="heading-step-2-set-up-your-project">Step 2: Set up your project</h3>
<p>Create a new SwiftUI project. For the purpose of this tutorial, we’ll use an iOS app – but you can do this with any app. Select Swift as the language and SwiftUI for the interface.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047209636/75cad7d3-f403-4285-8209-fd2bb65418e5.png" alt="App Creation Screen" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047246793/3491aa5d-c35c-4157-adf2-789fe5e9cd96.png" alt="Basic SwiftUI App after it's created" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Add the <code>openapi.yaml</code> file we just created to this project. (You can also create this file in Xcode and copy, paste from the script above.)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047290178/fc31b759-b1c4-4b48-b58b-d90109614cd0.png" alt="Adding the openapi.yaml file to our project" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, add the following swift packages to the project. (<strong>Note: Please read the entire section about adding packages before you proceed.</strong>)</p>
<ol>
<li><p>Swift OpenAPI Generator – <a target="_blank" href="https://github.com/apple/swift-openapi-generator">https://github.com/apple/swift-openapi-generator</a> – The Core Generator Plugin.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047376776/b1d7b2d0-9b1f-45c8-8c74-4395a4c80dd9.png" alt="Adding Swift OpenAPI Generator to our Project" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047411622/c6eca0c0-538e-40b5-a9b0-9fa044b60694.png" alt="Making sure that no targets are selected for the OpenAPIGenerator" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
</li>
<li><p>Swift OpenAPI Runtime – <a target="_blank" href="https://github.com/apple/swift-openapi-runtime">https://github.com/apple/swift-openapi-runtime</a> – This contains the common types and protocols used by the code generated by the generator plugin.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047457532/846ec488-cfed-4ca5-811b-c38dba0aaf30.png" alt="Adding OpenAPIRuntime to our project" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
</li>
<li><p>Swift OpenAPI URLSession – <a target="_blank" href="https://github.com/apple/swift-openapi-urlsession">https://github.com/apple/swift-openapi-urlsession</a> – This is a transport layer that allows the generated code to use the Apple URLSession to make network requests. </p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753047476699/9556047e-cc33-44b4-9980-0c692d4c1d01.png" alt="Adding OpenAPIURLSession to our Project" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
</li>
</ol>
<p>One major caveat to note here when adding these packages is that <strong>The Swift OpenAPI Generator</strong> should <strong>not</strong> be added to your project target. This is because we’re only using this to generate the code, but we’re not using it in the app.</p>
<p>If you get this error: <code>swift-openapi-generator/Sources/_OpenAPIGeneratorCore/PlatformChecks.swift:21:5 _OpenAPIGeneratorCore is only to be used by swift-openapi-generator itself—your target should not link this library or the command line tool directly.</code> – then you made this mistake.</p>
<p>The easiest way to fix this is removing the package and adding it again. Or you can go to <code>Project → Target → Build Phases → Link Binary with Libraries → Remove Swift OpenAPI Generator</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753048265985/100428a4-e46a-4aa1-8fcf-4c74e26ffed6.png" alt="Where to check if you encounter that error" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now that we added these generator and runtime plugins, we need to give the generator some instructions on what to generate. You can do this with an <code>openapi-generator-config.yaml</code> file. For our project, use the following file. It’s really simple:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">generate:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">types</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">client</span>
</code></pre>
<p>This tells our generator to generate the <strong>types</strong> – the swift structs, enums, and so on from the schema section of the file, and the <strong>client</strong> – the main class which interacts with the networking logic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753048237863/963d6bc0-a368-427b-add3-75dcf4bd3edf.png" alt="openapi-generator-config.yaml file" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Save this into an <code>openapi-generator-config.yaml</code> file as shown.</p>
<p>And finally, we want the generator to run whenever we want to build this application/target. We can specify this in the Build Phases tab of the target. Under the “ Target → Build Phases → Run Build Tool Plug-ins” , add the OpenAPIGenerator Plugin.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753048334094/24e20adc-c6e0-4d66-965b-19d476a5ffd3.png" alt="Adding the generator in the build phase" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The first time the project is built after setting this, Xcode will display a security dialog. This will let us “Trust and Enable” for this plugin. It’s a one time confirmation that gives this plugin the permission required to run during the build process.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753048374405/6ff51ff4-d33a-4920-b95f-9fda0ee0aef4.png" alt="Trust and Enable security dialog for the generator" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>As soon as you build the second time after giving these permissions, you will generate the files. You might not see any changes in the Xcode window itself. But if you’re curious to see the result, go to this folder. </p>
<p><code>DerivedData →  &lt;ProjectName&gt;*identifier → Build → intermediates.noindex → BuildToolPluginIntermediates → &lt;TargetName&gt;.output → &lt;TargetName&gt; → OpenAPIGenerator → GeneratedSources</code></p>
<p>More on derived data folder here: <a target="_blank" href="https://gayeugur.medium.com/derived-data-2e9468c6da9b">https://gayeugur.medium.com/derived-data-2e9468c6da9b</a> if you’re curious.</p>
<p>Keep in mind that this location might vary based on Xcode version, OpenAPI version, and your project settings. But you don’t need to worry about the file location.</p>
<p>You will see three files called Client.swift, Types.swift, and Server.swift.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753048508703/8fcec6bf-ad86-47de-b57c-7dca22256e06.png" alt="Generated Files" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>These are the files that the our generator created and populated with the types and functions we need.</p>
<p>In the next section, we discuss how to use these files to make calls to the server.</p>
<h3 id="heading-step-3-write-a-wrapper">Step 3: Write a wrapper</h3>
<p>While it’s certainly possible to make the calls to server using just the generated code (<code>Client</code>) type throughout our application, a more maintainable approach is to use a wrapper around these types. This will provide a stable, clean interface for the rest our our app to use, and it decouples feature code from the generated code.</p>
<p>I can hear you thinking: “Wait a second. Isn’t the entire purpose of generating this code to avoid this boilerplate abstraction?”</p>
<p>While it adds some abstraction on top of the generated code, it’s valuable to have this for number of reasons. Here are but a few of them:</p>
<ol>
<li><p>Better naming. The generated <code>Post</code> struct right now will be called <code>Components.Schemas.Post</code>.</p>
</li>
<li><p>If you ever want to move away from the generator, an abstraction is really helpful.</p>
</li>
<li><p>If you want to Mock this server call, you can do this via the abstraction.</p>
</li>
<li><p>UI Optimization. You might want to flatten the structure of a model to reduce the number of computed variables in there, and so on.</p>
</li>
</ol>
<p>So, we want to wrap this around a file called <code>WebService.swift</code>:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// WebService.swift</span>
<span class="hljs-keyword">import</span> Foundation
<span class="hljs-keyword">import</span> OpenAPIURLSession

<span class="hljs-comment">// A clean, app-specific Post model.</span>
<span class="hljs-comment">// This decouples views from the generated types.</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AppPost</span>: <span class="hljs-title">Identifiable</span>, <span class="hljs-title">Codable</span> </span>{
    <span class="hljs-keyword">let</span> id: <span class="hljs-type">Int</span>
    <span class="hljs-keyword">let</span> title: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> body: <span class="hljs-type">String</span>
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebService</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> client: <span class="hljs-type">Client</span>

    <span class="hljs-keyword">init</span>() {
        <span class="hljs-comment">// The server URL and transport are from the generated code.</span>
        <span class="hljs-comment">// `Servers.Server1.url()` corresponds to the first URL in the `servers` array of the spec.</span>
        <span class="hljs-keyword">self</span>.client = <span class="hljs-type">Client</span>(
            serverURL: <span class="hljs-keyword">try</span>! <span class="hljs-type">Servers</span>.<span class="hljs-type">Server1</span>.url(),
            transport: <span class="hljs-type">URLSessionTransport</span>()
        )
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getPosts</span><span class="hljs-params">()</span></span> async <span class="hljs-keyword">throws</span> -&gt; [<span class="hljs-type">AppPost</span>] {
        <span class="hljs-comment">// Call the generated method, which was named using `operationId`.</span>
        <span class="hljs-keyword">let</span> response = <span class="hljs-keyword">try</span> await client.getPosts(.<span class="hljs-keyword">init</span>())

        <span class="hljs-comment">// The generated response is a type-safe enum covering all documented status codes.</span>
        <span class="hljs-keyword">switch</span> response {
        <span class="hljs-keyword">case</span>.ok(<span class="hljs-keyword">let</span> okResponse):
            <span class="hljs-comment">// The body is also a type-safe enum for different content types.</span>
            <span class="hljs-keyword">switch</span> okResponse.body {
            <span class="hljs-keyword">case</span>.json(<span class="hljs-keyword">let</span> posts):
                <span class="hljs-comment">// Map the generated `Components.Schemas.Post` to our clean `AppPost` model.</span>
                <span class="hljs-keyword">return</span> posts.<span class="hljs-built_in">map</span> { post <span class="hljs-keyword">in</span>
                    <span class="hljs-type">AppPost</span>(id: post.id, title: post.title, body: post.body)
                }
            }
        <span class="hljs-comment">// The generator forces the handling of other documented responses.</span>
        <span class="hljs-comment">// Our simple spec only has a 200, so any other response is undocumented.</span>
        <span class="hljs-keyword">case</span>.undocumented(statusCode: <span class="hljs-keyword">let</span> statusCode, <span class="hljs-number">_</span>):
            <span class="hljs-keyword">throw</span> <span class="hljs-type">URLError</span>(.badServerResponse, userInfo: [<span class="hljs-string">"statusCode"</span>: statusCode])
        }
    }
}
</code></pre>
<p>Let’s go through this file to understand what we’re doing.</p>
<p>First, we import <code>OpenAPIUrlSession</code> along with <code>Foundation</code>. This allows us to call the server, get a response and parse that response.</p>
<p>Next, we define the new <code>AppPost</code> struct. This is meant to be the representation of a <code>Post</code> in the App. In the generated <code>Types.Swift</code> file, we have the generated <code>Post</code> structure. This is defined as:</p>
<pre><code class="lang-swift"><span class="hljs-comment">/// - Remark: Generated from `#/components/schemas/Post`.</span>
        <span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Post</span>: <span class="hljs-title">Codable</span>, <span class="hljs-title">Hashable</span>, <span class="hljs-title">Sendable</span> </span>{
            <span class="hljs-comment">/// - Remark: Generated from `#/components/schemas/Post/userId`.</span>
            <span class="hljs-keyword">internal</span> <span class="hljs-keyword">var</span> userId: <span class="hljs-type">Swift</span>.<span class="hljs-type">Int</span>
            <span class="hljs-comment">/// - Remark: Generated from `#/components/schemas/Post/id`.</span>
            <span class="hljs-keyword">internal</span> <span class="hljs-keyword">var</span> id: <span class="hljs-type">Swift</span>.<span class="hljs-type">Int</span>
            <span class="hljs-comment">/// - Remark: Generated from `#/components/schemas/Post/title`.</span>
            <span class="hljs-keyword">internal</span> <span class="hljs-keyword">var</span> title: <span class="hljs-type">Swift</span>.<span class="hljs-type">String</span>
            <span class="hljs-comment">/// - Remark: Generated from `#/components/schemas/Post/body`.</span>
            <span class="hljs-keyword">internal</span> <span class="hljs-keyword">var</span> body: <span class="hljs-type">Swift</span>.<span class="hljs-type">String</span>
            <span class="hljs-comment">/// Creates a new `Post`.</span>
            <span class="hljs-comment">///</span>
            <span class="hljs-comment">/// - Parameters:</span>
            <span class="hljs-comment">///   - userId:</span>
            <span class="hljs-comment">///   - id:</span>
            <span class="hljs-comment">///   - title:</span>
            <span class="hljs-comment">///   - body:</span>
            <span class="hljs-keyword">internal</span> <span class="hljs-keyword">init</span>(
                userId: <span class="hljs-type">Swift</span>.<span class="hljs-type">Int</span>,
                id: <span class="hljs-type">Swift</span>.<span class="hljs-type">Int</span>,
                title: <span class="hljs-type">Swift</span>.<span class="hljs-type">String</span>,
                body: <span class="hljs-type">Swift</span>.<span class="hljs-type">String</span>
            ) {
                <span class="hljs-keyword">self</span>.userId = userId
                <span class="hljs-keyword">self</span>.id = id
                <span class="hljs-keyword">self</span>.title = title
                <span class="hljs-keyword">self</span>.body = body
            }
            <span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">CodingKeys</span>: <span class="hljs-title">String</span>, <span class="hljs-title">CodingKey</span> </span>{
                <span class="hljs-keyword">case</span> userId
                <span class="hljs-keyword">case</span> id
                <span class="hljs-keyword">case</span> title
                <span class="hljs-keyword">case</span> body
            }
        }
</code></pre>
<p>As you can see, our <code>AppPost</code> struct is different from this generated type. We omit the <code>userId</code> since we do not care about it (at least for now).</p>
<p>Back to the <code>WebService</code> class, we see a <code>client</code> attribute. This is a generated type variable that will let us interact with the servers. In the initializer of the <code>WebService</code> class, we create a new <code>Client</code> using the first server URL we specified in the schema and use the <code>URLSessionTransport</code> object for making these calls.</p>
<p>We then define our methods. In this case, our <code>getPosts()</code> function which returns <code>[AppPost]</code> array.</p>
<p><code>let response = try await client.getPosts(.init())</code> will call the function <code>getPosts()</code> on the <code>Client</code> object. The <code>Client.getPosts()</code> function here takes in an input struct called <code>Operations.getPosts.Input</code> which is initialized by the <code>.init()</code> passed here.</p>
<p>This generated response is a type-safe enum covering all documented codes. (Currently only <code>200</code> in our yaml file). So, we use a simple switch to look at both these cases and further use more switch statements to get the proper response. You can see how much easier this is than to parse the response manually.</p>
<p>Once we get the <code>Components.Schemas.Post</code> response, we map and convert it into <code>[AppPost]</code> array and return it.</p>
<p>Now, let’s use this wrapper to display data in our app.</p>
<h3 id="heading-step-4-call-the-wrapper-and-display-the-data">Step 4: Call the wrapper and display the data</h3>
<p>We’re at the final step now. We’ll use the wrapper we created to display the fetched posts. We’ll also use a state variable to store our <code>AppPost</code> array in our <code>ContentView</code> view. We’ll then call <code>getPosts()</code> when the view is first displayed to the user.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// ContentView.swift</span>
<span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> posts: [<span class="hljs-type">AppPost</span>] = []
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> errorMessage: <span class="hljs-type">String?</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> webService = <span class="hljs-type">WebService</span>()

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">NavigationStack</span> {
            <span class="hljs-type">List</span>(posts) { post <span class="hljs-keyword">in</span>
                <span class="hljs-type">VStack</span>(alignment:.leading, spacing: <span class="hljs-number">8</span>) {
                    <span class="hljs-type">Text</span>(post.title)
                       .font(.headline)
                    <span class="hljs-type">Text</span>(post.body)
                       .font(.subheadline)
                       .foregroundColor(.secondary)
                }
               .padding(.vertical, <span class="hljs-number">4</span>)
            }
           .navigationTitle(<span class="hljs-string">"Posts"</span>)
           .task {
                await loadPosts()
            }
           .overlay {
                <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> errorMessage {
                    <span class="hljs-type">ContentUnavailableView</span>(<span class="hljs-string">"Error"</span>, systemImage: <span class="hljs-string">"xmark.octagon"</span>, description: <span class="hljs-type">Text</span>(errorMessage))
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> posts.isEmpty {
                    <span class="hljs-type">ProgressView</span>()
                }
            }
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">loadPosts</span><span class="hljs-params">()</span></span> async {
        <span class="hljs-keyword">self</span>.errorMessage = <span class="hljs-literal">nil</span>
        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">self</span>.posts = <span class="hljs-keyword">try</span> await webService.getPosts()
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-keyword">self</span>.errorMessage = error.localizedDescription
        }
    }
}

#<span class="hljs-type">Preview</span> {
    <span class="hljs-type">ContentView</span>()
}
</code></pre>
<p>You can see the dummy posts in the Preview. As you can see, all we had to do was call the <code>webService.getPosts()</code> to populate the variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753053957409/b89d50e8-ab73-4484-beda-3a328a575144.png" alt="Simulator Run of the app showing the fetched posts" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You might be thinking that this is a lot of setup for a simple struct like <code>Post</code> for which we had to create a wrapper called <code>AppPost</code> anyway. But if you had ten types like this and twenty endpoints to call? You wouldn’t have to deal with a lot of repetitive, error-prone code.</p>
<h2 id="heading-potential-pitfalls">Potential Pitfalls</h2>
<p>Unfortunately, no process is perfect. You might still face a lot of issues with generated code and this method. I’ve listed some of them here and how to deal with them.</p>
<h3 id="heading-verbose-or-ugly-generated-code">Verbose or ugly generated code</h3>
<p>If you have very verbose or ugly generated code, the problem is almost always the missing <code>operationId</code> for an API path. If you don’t specify one, the generator must create a name from the path and the HTTP method with results in long unwieldy names. Adding a clear <code>operationId</code> will mitigate this issue.</p>
<h3 id="heading-large-specs-and-performance-issues">Large Specs and Performance Issues</h3>
<p>If you have a very large Spec file, generating a client for this entire specification can significantly increase the compile time. It can also result in absolutely massive <code>Types.swift</code> and <code>Client.swift</code> files.</p>
<p>There is a filter option in the <code>openapi-generator-config.yaml</code> file that will allow the generator to include only parts of the spec that are relevant to the application to improve build times and so on. But if you want everything in an API that has hundreds of endpoints, the only way to reduce compile times is to avoid regenerating this every time and decouple this step from the regular build process.</p>
<h3 id="heading-unsupported-spec-features">Unsupported Spec Features</h3>
<p>While the swift package, <code>swift-openapi-generator</code>, is robust, it does not support all the features included in the specification. I had issues with some features of the newer spec version ( <code>3.1.1</code> and had to downgrade to <code>3.0.3</code> to make it work well ).</p>
<p>There are also known issues like lack of support for certain types of recursive schemas. Sometimes, the generator errors out and fails and some other times, it generates incomplete types – which can result in a few hours of debugging (I speak from experience).</p>
<p>In any case, knowing the limits of this generator can be helpful in avoiding issues it might cause. Also keep in mind that it is always getting better thanks to its open source nature.</p>
<h2 id="heading-conclusion-embrace-spec-driven-development">Conclusion: Embrace Spec-Driven Development</h2>
<p>In this guide, you navigated the journey of adopting <code>swift-openapi-generator</code> – from understanding the power of API contracts to building a functional SwiftUI app. You also learned about the real life challenges of this process. While there is an initial learning curve, the benefits of this approach are profound.</p>
<p>The core tenet of this approach is to foster more disciplined and more robust method for building applications. By making the OpenAPI document the single source of truth, you make sure that both the frontend and backend are perfectly in sync in perpetuity.</p>
<p>Using this approach also results in more type-safe, maintainable code. The result is less time spent on writing boilerplate and debugging random integration errors and more time spent creating the app itself.</p>
<p>For developers ready to explore further, please checkout the official <code>swift-openapi-generator</code> repository on Github here: <a target="_blank" href="https://github.com/apple/swift-openapi-generator">https://github.com/apple/swift-openapi-generator</a>.</p>
<p>You can follow me on <a target="_blank" href="https://github.com/sravankaruturi">GitHub</a> and <a target="_blank" href="https://hashnode.com/@sravankaruturi">Hashnode</a> for my other posts and projects.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Apple Code Signing Handbook ]]>
                </title>
                <description>
                    <![CDATA[ In this handbook, I’ll demystify the Apple app code signing process. Apple's ecosystem is powerful, but its distribution mechanisms – with various identifiers, certificates, and profiles – can appear complex. This guide attempts to make that journey ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/apple-code-signing-handbook/</link>
                <guid isPermaLink="false">68489084490c709f6b3070ed</guid>
                
                    <category>
                        <![CDATA[ Apple ]]>
                    </category>
                
                    <category>
                        <![CDATA[ iOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Apps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sravan Karuturi ]]>
                </dc:creator>
                <pubDate>Tue, 10 Jun 2025 20:07:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749585600223/49e9c922-0b5d-4a98-a619-eedfd7a8b617.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this handbook, I’ll demystify the Apple app code signing process. Apple's ecosystem is powerful, but its distribution mechanisms – with various identifiers, certificates, and profiles – can appear complex. This guide attempts to make that journey more manageable and straightforward for you.</p>
<p>Throughout this handbook, you will learn how to:</p>
<ul>
<li><p>Correctly establish and manage an app's unique identity.</p>
</li>
<li><p>Understand the roles of different Apple developer certificates and how to create and manage them.</p>
</li>
<li><p>Differentiate between various types of provisioning profiles and know when to use each one.</p>
</li>
</ul>
<p>This guide is geared towards new developers who want to learn how the code signing process works, but it should also be useful experienced developers who want or need to refresh their memory.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>While there are no hard prerequisites to understanding the certificates, bundles, and provisioning profiles for distributing on Apple platforms, it helps to have an Apple developer account to follow along.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-app-ids-bundle-ids-your-apps-identity">App IDs, Bundle IDs – Your App’s Identity</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-distribution-a-deep-dive-into-certificates">Understanding Distribution: A Deep Dive into Certificates</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-bridge-between-everything-provisioning-profiles">Bridge between everything: Provisioning Profiles</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-device-management-development-and-ad-hoc-builds">Device management – Development and Ad Hoc Builds</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-possibilities-enabling-capabilities-and-services">Possibilities: Enabling Capabilities and Services</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-app-ids-bundle-ids-your-apps-identity"><strong>App IDs, Bundle IDs — Your App’s Identity</strong></h2>
<p>The Bundle ID and the corresponding App ID registered with Apple form the basis of an application’s identity. Establishing these correctly from the beginning is very important, as errors or misconfigurations here can lead to significant complications down the line, particularly once you’ve submitted your app to App Store Connect.</p>
<h3 id="heading-understanding-cfbundleidentifier-bundle-id">Understanding <code>CFBundleIdentifier</code> (Bundle ID)</h3>
<h4 id="heading-what-is-the-bundle-id">What is the “Bundle ID”?</h4>
<p>Think of the Bundle ID as a unique name or a fingerprint for your app. The <code>CFBundleIdentifier</code>, more commonly known as the <strong>Bundle ID</strong>, is a string that uniquely identifies your application.</p>
<p>This identifier is not just a name – it serves multiple crucial purposes.</p>
<ul>
<li><p>The operating system relies on it to apply specific preferences and settings to an app.</p>
</li>
<li><p>This is used to launch the application from other apps etc.</p>
</li>
<li><p>It plays an essential role in the validation of an app's code signature, ensuring the app's integrity and authenticity.</p>
</li>
<li><p>The Bundle ID defined in an app's Info.plist file must exactly match the Bundle ID registered for the app in App Store Connect for successful submission and distribution.</p>
</li>
</ul>
<p>The Bundle ID string must adhere to specific character limitations: it can only contain alphanumeric characters <code>A-Z, a-z, 0-9</code>, hyphens <code>-</code>, and periods <code>.</code>. It's important to note that Bundle IDs are treated as <strong>case-insensitive</strong> by the system.</p>
<h3 id="heading-how-to-choose-and-format-your-bundle-id-reverse-dns-and-best-practices">How to Choose and Format Your Bundle ID (Reverse-DNS and Best Practices)</h3>
<p>Apple highly recommends, and it is standard practice, to use a reverse-DNS (Domain Name System) format for Bundle IDs.</p>
<p>A common example would be <code>com.yourcompanyname.appname</code>. This convention leverages the global uniqueness of domain names to help ensure the global uniqueness of Bundle IDs.</p>
<p>If an organization uses its unique domain name (for example, <code>sravan.gg</code> becomes <code>gg.sravan</code> ) as the prefix, and the app name is unique within that organization, the resulting Bundle ID (for example, <code>gg.sravan.mycoolapp</code> ) is highly likely to be unique worldwide.</p>
<p><strong>Sidenote</strong>: While Xcode won’t stop you from creating something like <code>com.google.mapping</code> or something like that even if you don’t work at Google, this will most likely get rejected when it goes through the AppStore review process. This is because this implies ownership of that domain. So, while it’s technically possible when starting out, you shouldn’t use domains that don’t belong to you.</p>
<p>The fundamental nature of the Bundle ID as a unique, system-wide identifier – coupled with its immutability after an app is first uploaded to App Store Connect – means that you should treat its selection with the same seriousness as choosing a <strong>permanent, unchangeable identifier</strong> for a critical entity. A mistake in the Bundle ID after this point can necessitate creating an entirely new app listing on the App Store.</p>
<h3 id="heading-app-ids-in-the-apple-developer-portal-explicit-vs-wildcard">App IDs in the Apple Developer Portal: Explicit vs. Wildcard</h3>
<h4 id="heading-which-one-do-you-need">Which One Do You Need?</h4>
<p>In the Apple Developer Portal, developers register an "App ID." This App ID is a record that links one or more applications from a single development team to specific app services (capabilities) and is used in provisioning profiles. We’ll learn more about this in the following sections.</p>
<p>There are two main types of App IDs:</p>
<ul>
<li><p><strong>Explicit App ID:</strong> This type is used for a single application. The Bundle ID specified within an explicit App ID must be an exact match for the CFBundleIdentifier in the app's Info.plist file (for example, <code>com.mycompany.myapp</code>). Explicit App IDs are required for apps that use many of Apple's specific services and capabilities, such as In-App Purchases (which are enabled by default for explicit App IDs), Push Notifications, iCloud, HealthKit, and Sign in with Apple.</p>
</li>
<li><p><strong>Wildcard App ID:</strong> This type can be used for a set of applications that share a common Bundle ID prefix. It contains an asterisk (*) as the last part of its Bundle ID string (for example, <code>com.mycompany.*</code>). This wildcard App ID would match any app whose Bundle ID starts with <code>com.mycompany.</code>, such as <a target="_blank" href="http://com.mycompany.app"><code>com.mycompany.app</code></a> or <code>com.mycompany.utility</code>. But you can’t use wildcard App IDs if the app requires services or capabilities that mandate an explicit App ID.</p>
</li>
</ul>
<p>The choice between an explicit and a wildcard App ID has significant implications. The App ID acts as a central registration point, and the capabilities are "enabled" for this registration – more on capabilities later in this handbook.</p>
<p>You can think of an explicit App ID as a specific key designed to unlock extra "keyholes" (capabilities). A wildcard App ID, being more generic, might not fit these extra keyholes. If you choose a wildcard App ID initially for convenience, but you need a feature requiring an explicit App ID (like Push Notifications) later, you’ll be forced to create a new explicit App ID and reconfigure associated settings and provisioning profiles.</p>
<p>So, make sure you carefully consider your current and future app features when selecting an App ID type. The following table provides a quick comparison**.**</p>
<p>My personal recommendation is always go with explicit App Ids unless you need the flexibility of wildcard app ids.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Explicit App ID</strong></td><td><strong>Wildcard App ID</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Bundle ID Match</strong></td><td>Exact match (for example, <a target="_blank" href="http://com.foo.bar">com.foo.bar</a>)</td><td>Suffix match (for example, <a target="_blank" href="http://com.foo">com.foo</a>.*)</td></tr>
<tr>
<td><strong>Use Case</strong></td><td>Single app</td><td>Set of apps with similar base ID</td></tr>
<tr>
<td><strong>Capabilities</strong></td><td>Supports all capabilities</td><td>Limited (cannot use services requiring explicit IDs)</td></tr>
<tr>
<td><strong>Uniqueness</strong></td><td>Globally unique identifier for one specific app</td><td>Identifies a group of apps</td></tr>
</tbody>
</table>
</div><h3 id="heading-step-by-step-how-to-register-your-app-id-in-the-apple-developer-portal">Step-by-Step: How to Register Your App ID in the Apple Developer Portal</h3>
<p>To register an App ID, an you’ll need an <strong>Apple Developer Program membership</strong>. Also, the actions must be performed by someone with an Account Holder or Admin role.</p>
<p>The process is as follows:</p>
<ol>
<li><p>Sign in to the Apple Developer Portal and navigate to "Certificates, Identifiers &amp; Profiles," then select "Identifiers" from the sidebar.</p>
</li>
<li><p>Click the “Add button (+)” to create a new identifier.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748642247245/a24b527f-e810-4a9c-b75a-dcd3d189b1d1.png" alt="Picture depicting the Add button" class="image--center mx-auto" width="1130" height="404" loading="lazy"></p>
</li>
<li><p>Select "App IDs" from the list of options and click "Continue."</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748642283885/851f64f3-e608-4fb7-9f31-bd30adb64beb.png" alt="851f64f3-e608-4fb7-9f31-bd30adb64beb" class="image--center mx-auto" width="1628" height="704" loading="lazy"></p>
</li>
<li><p>Make sure that the "App" type is selected (it usually is by default) and click "Continue."</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748642318142/a7b28529-bbe6-4240-953e-836de3e948ac.png" alt="App type selection" class="image--center mx-auto" width="1526" height="766" loading="lazy"></p>
</li>
<li><p>Enter a "Description" for the App ID. This is for your reference within the portal (for example, "My very cool App ID").</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748642392862/a5322cf5-3d75-4b0b-93bf-d46dd1ce8afe.png" alt="App Id registration screen" class="image--center mx-auto" width="2446" height="698" loading="lazy"></p>
</li>
<li><p>Choose the "App ID Type": "Explicit" or "Wildcard."</p>
</li>
<li><p>For an "Explicit App ID," enter the exact Bundle ID that will be used in your Xcode project (for example, <code>com.yourcompany.yourapp</code>). For a "Wildcard App ID," enter a Bundle ID suffix ending with an asterisk (for example, <code>com.yourcompany.*</code>).</p>
</li>
<li><p>Scroll down to the "Capabilities" section and select the checkboxes for any app services your app will use. Some capabilities might require further configuration at this stage or later. (Again, we’ll cover app capabilities in more detail later on).</p>
</li>
<li><p>Click "Continue," review all the details carefully, and then click "Register" to finalize the App ID creation.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748642432661/2052a435-ed0e-404a-9178-7d6541fc9421.png" alt="Confirm the App ID screen" class="image--center mx-auto" width="2486" height="708" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-how-to-manage-your-bundle-id-xcode-app-store-connect-and-the-point-of-no-return">How to Manage Your Bundle ID: Xcode, App Store Connect, and the Point of No Return</h3>
<p>The Bundle ID specified in an Xcode project is critical. To set it:</p>
<ol>
<li><p>In the Xcode project navigator, select the target for your app.</p>
</li>
<li><p>Open the "Signing &amp; Capabilities" tab.</p>
</li>
<li><p>Expand the "Signing" section.</p>
</li>
<li><p>In the "Bundle Identifier" text field, enter the Bundle ID. This identifier must precisely match the Bundle ID associated with an explicit App ID registered in the Developer Portal, or conform to the pattern of a wildcard App ID if applicable.</p>
</li>
</ol>
<p>It's important to understand the difference between the "Bundle ID" (or <code>CFBundleIdentifier</code>) in the Xcode project and the "App ID" registered in the Developer Portal. The "App ID" in the developer portal is an entity that <em>contains</em> a “Bundle ID” string (either explicit or wildcard). The string in your Xcode project's "Bundle Identifier" field must match this contained string.</p>
<p>When preparing for distribution via TestFlight or the App Store, you’ll need to create an app record in App Store Connect. The Bundle ID you enter during this app record creation must exactly match the Bundle ID in the Xcode project.</p>
<h4 id="heading-a-critical-warning-immutability-after-first-upload">A Critical Warning: Immutability After First Upload</h4>
<p>This is a point of no return: Once you upload a build of an app to App Store Connect, the Bundle ID for that app record <strong>cannot be changed</strong>.</p>
<p>In addition, after an upload, you can’t delete the associated explicit App ID registered in the Developer Portal. This immutability highlights the need for <em>careful planning and verification</em> of the Bundle ID before any uploads occur.</p>
<p>If you prefer programmatic management or automation, the App Store Connect API provides resources for managing Bundle IDs. You can <a target="_blank" href="https://developer.apple.com/documentation/appstoreconnectapi">read more on that here</a>.</p>
<h2 id="heading-understanding-distribution-a-deep-dive-into-certificates"><strong>Understanding Distribution: A Deep Dive into Certificates</strong></h2>
<h3 id="heading-what-are-certificates">What are Certificates?</h3>
<p>Certificates are digital credentials that verify a <strong>developer's identity</strong> – that is, you – to Apple and, by extension, to the app users.</p>
<p>They are fundamental to Apple's code signing process, which is mandatory for all apps to ensure they originate from a <strong>known source</strong> and have not been tampered with since being signed.</p>
<h3 id="heading-what-is-code-signing-ensuring-trust-and-integrity">What is Code Signing: Ensuring Trust and Integrity</h3>
<p>Code signing is you as a developer signing the app with your signature. It is the process of attaching a digital signature to an app's code. This signature assures users of two key things:</p>
<ol>
<li><p><strong>Authenticity:</strong> The app was created by an identified Apple developer (an individual or a team).</p>
</li>
<li><p><strong>Integrity:</strong> The app's code has not been altered or corrupted since it was signed by the developer.</p>
</li>
</ol>
<p>The process involves using a private key, securely held by the developer (you), to create the signature. The corresponding public key, embedded within the developer's certificate (issued by Apple), is used by the system to verify this signature.</p>
<p>This system of identity verification and integrity checking is crucial. The developer's certificate, issued by Apple as a Certificate Authority (CA), vouches for their identity. The code signing process, using hashing and encryption, ensures that any modification to the code after signing would invalidate the signature.</p>
<p>For app developers, benefits of code signing include removing warnings on macOS for apps distributed outside the Mac App Store, providing a smoother user experience. It is a mandatory requirement for listing applications on any of Apple's App Stores. It also enhances security of the app as it acts as a deterrent against malicious tampering.</p>
<h3 id="heading-types-of-certificates-development-distribution-and-developer-id">Types of Certificates: Development, Distribution, and Developer ID</h3>
<p>Apple provides different types of certificates for various stages of development and methods of distribution. Each of them has a distinct role to play throughout the app development process.</p>
<h4 id="heading-1-development-certificates-for-example-apple-development">1. Development Certificates (for example, "Apple Development"):</h4>
<ul>
<li><p><strong>Purpose:</strong> Used to sign apps during the development phase, allowing them to be installed and run on a limited number of <em>registered test devices</em> and simulators for debugging and testing.</p>
</li>
<li><p><strong>Identifies:</strong> Typically identifies an individual developer through their developer ID.</p>
</li>
<li><p><strong>Used with:</strong> Development provisioning profiles – more on this later.</p>
</li>
</ul>
<h4 id="heading-2-distribution-certificates-for-example-apple-distribution">2. Distribution Certificates (for example, "Apple Distribution"):</h4>
<ul>
<li><p><strong>Purpose:</strong> Used to sign apps intended for distribution, either through Ad Hoc methods (to a limited set of <em>registered testers</em>) or for submission to the App Store.</p>
</li>
<li><p><strong>Identifies:</strong> The development team via the team identifier.</p>
</li>
<li><p><strong>Use Cases:</strong></p>
<ol>
<li><p><strong>App Store:</strong> For signing the final version of an app that will be uploaded to App Store Connect for TestFlight beta testing or release on the App Store (iOS, macOS, tvOS, watchOS). These are used with App Store provisioning profiles – more on this later.</p>
</li>
<li><p><strong>Ad Hoc:</strong> For signing apps that will be distributed to a <em>limited number of registered test devices outside of the App Store or TestFlight</em>. These are used with Ad Hoc provisioning profile. More on this later.</p>
</li>
</ol>
</li>
</ul>
<h4 id="heading-3-developer-id-certificates-for-mac-apps-distributed-outside-the-mac-app-store">3. Developer ID Certificates (for Mac apps distributed outside the Mac App Store):</h4>
<ul>
<li><p><strong>Purpose:</strong> Specifically for macOS developers who wish to distribute their applications directly to users (for example, from their own website) rather than through the Mac App Store. Gatekeeper on macOS recognizes apps signed with a Developer ID certificate, assuring users that the app is from a known developer and has not been tampered with.</p>
</li>
<li><p><strong>Types:</strong></p>
<ol>
<li><p><strong>Developer ID Application:</strong> Used to sign the Mac application bundle (.app) itself.</p>
</li>
<li><p><strong>Developer ID Installer:</strong> Used to sign a Mac Installer Package (.pkg) that contains the signed application.</p>
</li>
<li><p><strong>Limit:</strong> Developers can create up to five Developer ID Application certificates and up to five Developer ID Installer certificates.</p>
</li>
</ol>
</li>
</ul>
<p>The following table summarizes these certificate types:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Certificate Type</strong></td><td><strong>Issued To</strong></td><td><strong>Primary Purpose</strong></td><td><strong>Used With Provisioning Profile Type</strong></td><td><strong>Key Use Cases</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Apple Development</td><td>Individual Dev ID</td><td>Develop &amp; debug on registered devices</td><td>Development</td><td>Xcode builds for local testing, running on personal/team test devices.</td></tr>
<tr>
<td>Apple Distribution</td><td>Team ID</td><td>Submit app to App Store / Ad Hoc distribution</td><td>App Store, Ad Hoc</td><td>Final builds for TestFlight, App Store submission, or QA/client Ad Hoc builds.</td></tr>
<tr>
<td>Developer ID Application</td><td>Team ID</td><td>Sign Mac app for distribution outside Mac App Store</td><td><strong>Developer ID Provisioning</strong> <strong>Profile</strong> if the app utilizes specific capabilities (e.g., Push Notifications, Associated Domains).</td><td>Distributing Mac software directly to users (for example, from website).</td></tr>
<tr>
<td>Developer ID Installer</td><td>Team ID</td><td>Sign Mac Installer Pkg for distribution outside Mac App Store</td><td>N/A. (The app inside the package may need a profile).</td><td>Distributing Mac software in a .pkg installer directly to users.</td></tr>
<tr>
<td>APNs / Service Keys (.p8)</td><td>Team ID</td><td>Secure communication with specific Apple services</td><td>N/A for app signing</td><td>Push Notifications, MusicKit, DeviceCheck and so on. (Token-based authentication)</td></tr>
</tbody>
</table>
</div><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748216973656/76df3f64-c84e-4195-a092-37c1143d8b1b.png" alt="Create a new certificate screen in App Store Connect" class="image--center mx-auto" width="1768" height="1384" loading="lazy"></p>
<h3 id="heading-how-to-create-an-apple-certificate-an-overview">How to Create an Apple Certificate – An Overview</h3>
<p>Here’s a general outline of how you create an Apple Certificate:</p>
<ul>
<li><p>Generate a Certificate Signing Request (CSR) on your Mac. (Yes you need a mac.)</p>
</li>
<li><p>You upload this CSR in AppStoreConnect as a part of creating the certificate.</p>
</li>
<li><p>Download the certificate from AppStoreConnect once it’s issued.</p>
</li>
<li><p>Install the certificate into your Keychain.</p>
</li>
</ul>
<p>Now we’ll go through each step in more detail. This part is very important, since we have to save some of the files generated locally or we lose the ability to transfer these certificates. This would mean revoking and re-issuing certificates (I have done this more times than I’d like to admit).</p>
<h4 id="heading-how-to-create-a-certificate-signing-request-csr">How to Create a Certificate Signing Request (CSR)</h4>
<p>A Certificate Signing Request (CSR) is a fancy name for an encrypted block of text containing information about who’s requesting the certificate (like your name and the public key). These are widely used in the cryptography world.</p>
<p>For our purposes, you’ll generate a CSR on your Mac and then submit it to Apple to request a digital certificate. The CSR generation process also creates a new public/private key pair on the Mac – the private key is stored in Keychain Access and is used for the eventual code signing.</p>
<p>To create a CSR using Keychain Access on macOS:</p>
<ol>
<li><p>Launch Keychain Access (you can find it at <code>/Applications/Utilities/</code> or use spotlight).</p>
</li>
<li><p>From the menu bar, choose Keychain Access &gt; Certificate Assistant &gt; Request a Certificate From a Certificate Authority.... (Here the Certificate Authority would be Apple).</p>
</li>
<li><p>In the dialog, enter your email address and a common name for the key (for example, "My Mac Key" or "[Your Name] Dev Key"). This name is primarily for your identification in the Keychain.</p>
</li>
<li><p>Leave the "CA Email Address" field empty – we won’t email it to the Certificate Authority (Apple).</p>
</li>
<li><p>Select the "Saved to disk" option and click "Continue".</p>
</li>
<li><p>Save the file, which will have a .certSigningRequest extension. The corresponding private key is now stored in the login keychain. <strong>This private key is irreplaceable by Apple and you must store it yourself.</strong></p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748288861336/50f20da3-69d9-476d-97e7-331f9b9b5c76.png" alt="Dialog for the CSR creation" class="image--center mx-auto" width="684" height="510" loading="lazy"></p>
<h4 id="heading-how-to-generate-and-download-your-apple-certificates">How to Generate and Download Your Apple Certificates</h4>
<p>Once you’ve created a CSR, you can request a certificate from the Apple Developer Portal:</p>
<ol>
<li><p>Navigate to "Certificates, Identifiers &amp; Profiles" and select "Certificates".</p>
</li>
<li><p>Click the add button (+).</p>
</li>
<li><p>Choose the desired certificate type</p>
</li>
<li><p>Follow the prompts, and when asked, upload the .certSigningRequest file generated earlier.</p>
</li>
<li><p>After Apple processes the request, the certificate will be available for download as a .cer file.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748289386364/78f46b4e-b232-4484-98c2-dcb75120fd61.png" alt="Prompt to upload the CSR after selecting the type of certificate" class="image--center mx-auto" width="1293" height="319" loading="lazy"></p>
</li>
</ol>
<p>To install the certificate, double-click the downloaded .cer file. It will be added to the Keychain Access application – usually appearing in the "login" keychain under the "My Certificates" category, where it should be paired with the private key generated during the CSR generation process earlier.</p>
<p>You can see my certificate and private key in the image below for reference.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748289120657/38f711dd-887a-4fae-844d-e389c65234cf.png" alt="An example of how your certificate and the private key will look like in the keychain" class="image--center mx-auto" width="905" height="44" loading="lazy"></p>
<p>To recap, the CSR certifies that you generated the request from your mac. The certificate certifies that Apple (in this case, an intermediary like the "Apple Worldwide Developer Relations Certification Authority") confirms that they verified the CSR and that it is indeed you who will sign with the certificate (<code>.cer</code>) file.</p>
<p>This is enforced by only you having access to the private key – if you lose it, you cannot use this certificate anymore.</p>
<p>So, if you use this certificate (and the private key) to sign an app, the app store / operating system knows that it is you for sure since Apple confirmed it.</p>
<h3 id="heading-how-to-store-your-keys-what-are-p12-files">How to Store Your Keys: What are .p12 Files?</h3>
<p>As I mentioned in the previous section, to code sign an app you need your certificate (containing the public key) and the corresponding private key. This is created along with the CSR, and you can find it in the <code>Keychain Access</code> app.</p>
<p>We call the combination of the certificate and the private key a digital identity. This proves your identity when you sign an app with them.</p>
<h4 id="heading-p12-files-personal-information-exchange">.p12 Files (Personal Information Exchange):</h4>
<p>A .p12 file is a password-protected archive format used to bundle a certificate along with its private key. Its primary purposes are:</p>
<ul>
<li><p>Backing up the digital identity in case you lose access to your Mac.</p>
</li>
<li><p>Transferring the digital identity to another Mac (for example, for another team member or a new development machine).</p>
</li>
<li><p>Providing the identity to automated build systems or third-party build services.</p>
</li>
</ul>
<p>Historically, I have stored the .p12 file on a shared drive with my team and shared the password to it verbally – you can also store it in a local backup disk.</p>
<p>Great. So how do you create one?</p>
<h4 id="heading-to-export-a-p12-file-from-keychain-access">To export a .p12 file from Keychain Access:</h4>
<ol>
<li><p>Open Keychain Access, select the "login" keychain, and go to the "My Certificates" category.</p>
</li>
<li><p>Locate the desired certificate. It should have an expandable disclosure triangle indicating an associated private key (look at the image of my certificate above).</p>
</li>
<li><p>Select <em>both</em> the certificate and its private key (or right-click the certificate and choose "Export").</p>
</li>
<li><p>Right-click and choose "Export [X] items...".</p>
</li>
<li><p>In the save dialog, choose the "Personal Information Exchange (.p12)" file format.</p>
</li>
<li><p>Assign a strong password to protect the .p12 file. This password will be required when importing the file elsewhere. It is crucial for security.</p>
</li>
<li><p>Save the file to a secure location.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748297124625/f9d2cfe0-3538-405e-8fb0-af08276c4326.png" alt="Image of exporting my certificate and private key as a .p12 file" class="image--center mx-auto" width="468" height="266" loading="lazy"></p>
</li>
</ol>
<h2 id="heading-bridge-between-everything-provisioning-profiles"><strong>Bridge Between Everything: Provisioning Profiles</strong></h2>
<p>Provisioning profiles are the final link between an App ID, developer certificates, and, in some cases, a list of specific test devices. They act as a permission slip, authorizing an app signed with a particular certificate to be installed and run either on designated devices or to be submitted to the App Store.</p>
<h3 id="heading-what-exactly-is-a-provisioning-profile">What Exactly is a Provisioning Profile?</h3>
<p>A provisioning profile is a <code>.mobileprovision</code> (for iOS / VisionOS) or <code>.provisionprofile</code> (for macOS) file that holds several key pieces of information:</p>
<ul>
<li><p><strong>The App ID:</strong> Specifies which application (or set of applications, if using a wildcard App ID) the profile applies to.</p>
</li>
<li><p><strong>Certificates:</strong> Contains one or more developer or distribution certificates that can be used to sign the app.</p>
</li>
<li><p><strong>Device UDIDs (for Development and Ad Hoc):</strong> For profiles intended for testing on specific devices, it includes a list of the Unique Device Identifiers (UDIDs) of those authorized devices – more on devices in the next section.</p>
</li>
<li><p><strong>Entitlements:</strong> A list of app services or capabilities (like Push Notifications, iCloud, App Groups) that the app is permitted to use. These are derived from the capabilities enabled for the <em>associated App ID</em>.</p>
</li>
</ul>
<p>You can open the file using <code>vim</code> or any editor to see parts of the content which include the App Id, Operating Systems, Certificates, and so on.</p>
<p>The operating system checks the provisioning profile at app launch to ensure the app is authorized to run on the current device and use the requested services. If the profile is missing, invalid, or doesn't match the app's signature or the device, the app will not launch.</p>
<p>They are difference from certificates, because certificates are tied to you as a developer. But provisioning profiles are to a specific app – with specific capabilities to a specific developer and maybe on specific devices.</p>
<p>If any of these change (let’s say you added a capability or your certificate expired, for example), you’ll need to generate the provisioning profile again. These are the files you will work with the most out of all the above, and any change can cause your profile to become invalid.</p>
<h3 id="heading-types-of-provisioning-profiles-development-ad-hoc-app-store-and-enterprise">Types of Provisioning Profiles: Development, Ad Hoc, App Store, (and Enterprise)</h3>
<h3 id="heading-types-of-provisioning-profiles-development-ad-hoc-app-store-and-enterprise-1"><strong>Types of Provisioning Profiles: Development, Ad Hoc, App Store, (and Enterprise)</strong></h3>
<p>Just like certificates, we have multiple types of provisioning profiles. Similar to certificates, there can be development and distribution provisioning profiles.</p>
<p>Since we also keep track of the devices a profile is supposed to run, we have several kinds of distribution profiles based on which devices it should run on.</p>
<p>We also have special profiles like “Enterprise” which will add additional capabilities (like main camera access on the Vision Pro) but will restrict your app distribution methods to enterprise only.</p>
<p>We will go over each of these types now. Feel free to skip to the one that you’re looking for.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Profile Type</strong></td><td><strong>Purpose</strong></td><td><strong>Required Certificate Type(s)</strong></td><td><strong>Device Registration Required?</strong></td><td><strong>Distribution Method</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Development</strong></td><td>Install &amp; debug on registered devices during development (need Xcode to install).</td><td>Development</td><td>Yes</td><td>Xcode run, local device deployment.</td></tr>
<tr>
<td><strong>Ad Hoc</strong></td><td>Distribute to a limited number of registered test devices (no need for Xcode).</td><td>Distribution</td><td>Yes</td><td>Manual install (for example, via link, email, MDM) for testers.</td></tr>
<tr>
<td><strong>App Store Connect</strong></td><td>Submit app to App Store Connect for TestFlight or App Store release.</td><td>Distribution</td><td>No</td><td>Upload to App Store Connect.</td></tr>
<tr>
<td><strong>Enterprise</strong></td><td>Distribute proprietary apps to employees within an organization.</td><td>Enterprise (Distribution)</td><td>No (subject to program terms)</td><td>Internal distribution (e.g., private portal, MDM).</td></tr>
<tr>
<td><strong>Developer ID</strong></td><td>Allows a macOS app that is distributed outside the App Store to use advanced features</td><td>Developer ID</td><td>No</td><td>Outside the Mac App Store (for example, a web page, USB, MDM )</td></tr>
</tbody>
</table>
</div><h4 id="heading-development-provisioning-profile"><strong>Development Provisioning Profile:</strong></h4>
<ul>
<li><p><strong>Allows</strong> an app to be installed and debugged on specific devices registered in the developer's account during the active development phase. More on device registration later.</p>
</li>
<li><p><strong>Contains</strong> an App ID, one or more development certificates, and a list of registered device UDIDs.</p>
</li>
<li><p><strong>Created</strong> manually in the Apple Developer Portal or generated automatically by Xcode if <code>Automatically manage signing</code> is enabled.</p>
</li>
</ul>
<h4 id="heading-ad-hoc-provisioning-profile"><strong>Ad Hoc Provisioning Profile:</strong></h4>
<ul>
<li><p><strong>Allows</strong> distribution of an app to a limited number of registered test devices <strong>without</strong> requiring Xcode for installation. This is ideal for distributing builds to QA teams, beta testers, or clients for feedback.</p>
</li>
<li><p><strong>Contains</strong> an App ID (often an explicit App ID, or an Xcode-managed one like <code>XC Wildcard</code> or <code>XC</code>), a single distribution certificate, and a list of registered device UDIDs.</p>
</li>
<li><p><strong>Created</strong> manually in the Developer Portal or managed by Xcode's automatic signing.</p>
</li>
</ul>
<h4 id="heading-app-store-connect-provisioning-profile"><strong>App Store Connect Provisioning Profile:</strong></h4>
<ul>
<li><p><strong>Required</strong> to sign an app for submission to App Store Connect. This is the pathway for distributing apps via TestFlight for broader beta testing and for official release on the App Store.</p>
</li>
<li><p><strong>Contains</strong> an explicit App ID (or an App ID that matches the app's bundle ID, including Xcode-managed App IDs), and a single distribution certificate. <em>Device UDIDs are not included in this profile type since this is meant for broader distribution.</em></p>
</li>
<li><p><strong>Created</strong> manually in the Developer Portal or managed by Xcode's automatic signing.</p>
</li>
</ul>
<h4 id="heading-enterprise-provisioning-profile"><strong>Enterprise Provisioning Profile:</strong></h4>
<ul>
<li><p>Exclusively for members of the <strong>Apple Developer Enterprise Program</strong>. It allows developers of these orgs to distribute proprietary, in-house applications directly to their employees, bypassing the public App Store.</p>
</li>
<li><p>Note: This program has stringent enrollment criteria and is strictly for internal distribution within the enrolled organization – these apps cannot be pushed to AppStore.</p>
</li>
</ul>
<h4 id="heading-developer-id-provisioning-profile"><strong>Developer ID Provisioning Profile:</strong></h4>
<ul>
<li><p><strong>Required</strong> to utilize certain Apple services or advanced capabilities like Push Notifications, CloudKit, Sign in with Apple, or specific iCloud services.</p>
</li>
<li><p><strong>Contains</strong> an App ID, a Developer ID distribution certificate, the entitlements authorized for the app.</p>
</li>
<li><p><strong>Created</strong> manually in the Developer Portal – will not be added automatically by Xcode’s automatic signing.</p>
</li>
</ul>
<h3 id="heading-how-to-create-and-manage-provisioning-profiles">How to Create and Manage Provisioning Profiles</h3>
<p>Creating and managing provisioning profiles usually requires an Account Holder or Admin role in the Apple Developer Program. You also need a configured App ID, the appropriate certificate(s), and for Development or Ad Hoc profiles, a list of registered device UDIDs.</p>
<p>If you are new developer, my recommendation is to read this article completely, then get back to this section once you have your devices setup.</p>
<p>General steps for manual creation in the Developer Portal:</p>
<ol>
<li><p>Navigate to "Certificates, Identifiers &amp; Profiles" and select "Profiles".</p>
</li>
<li><p>Click the add button (+).</p>
</li>
<li><p>Select the type of provisioning profile to create (for example, "iOS App Development," "Ad Hoc," "App Store").</p>
</li>
<li><p>Choose the App ID you’re targeting from the dropdown list.</p>
</li>
<li><p>Select the certificate(s) to include in the profile. Development profiles can include multiple development certificates – so you can include all the team member certificates here. Ad Hoc and App Store profiles include a single distribution certificate.</p>
</li>
<li><p>If creating a Development or Ad Hoc profile, select the registered devices to include.</p>
</li>
<li><p>Provide a name for the provisioning profile (this is for identification in the portal and Xcode).</p>
</li>
<li><p>Click "Generate" and then "Download" the <code>.mobileprovision</code> or <code>.provisionprofile</code> file.</p>
</li>
</ol>
<p>You need to make downloaded profiles available to Xcode. You can often do this by double-clicking the downloaded file or by refreshing profiles within Xcode's account settings (Preferences &gt; Accounts).</p>
<p>I really like Xcode's "Automatically manage signing" feature and it can simplify profile management by a lot. It creates and updates profiles as needed. But, understanding the manual process is crucial for troubleshooting because when things go wrong, it is straightforward to debug the issue with this knowledge.</p>
<p>Provisioning profiles will become invalid and require regeneration if:</p>
<ul>
<li><p>The capabilities of the associated App ID are changed – let’s say you added a new capability.</p>
</li>
<li><p>An included certificate expires or is revoked.</p>
</li>
<li><p>For Development/Ad Hoc profiles, if devices are added or removed from the registered list in a way that affects the profile's device set, or if the profile's own expiration date is reached. When such changes occur, you have to edit the profile (if possible) or delete it and recreate it in the Developer Portal, then re-download it and install it again. While this may seem like a complicated step, it’s straightforward if you do it a couple of times.</p>
</li>
</ul>
<h2 id="heading-device-management-development-and-ad-hoc-builds"><strong>Device Management — Development and Ad Hoc Builds</strong></h2>
<p>For testing applications on physical Apple hardware outside of Testflight or AppStore, you’ll need to register the Unique Device Identifiers (UDIDs) of your test devices with your Apple Developer account. This registration is a necessary step for creating Development and Ad Hoc provisioning profiles.</p>
<h3 id="heading-why-you-need-to-register-test-devices">Why You Need to Register Test Devices</h3>
<p>Development and Ad Hoc provisioning profiles are specifically tied to a list of registered devices. An app signed with this profile can be installed directly without going through App Store process. This means that you need to register devices you intend to develop on. This restricts bad faith actors from releasing apps widely without developer and App Store supervision.</p>
<p>The UUID of a device is like a physical address (think Mac Address). If you don’t include this in the provisioning profile you used to sign an app package, it cannot be installed on that device.</p>
<p>Let’s go over the steps to do that.</p>
<h3 id="heading-how-to-find-your-devices-udid-unique-device-identifier">How to Find Your Device's UDID (Unique Device Identifier)</h3>
<p>A UDID is a unique 40-character hexadecimal string (for older devices) or a 25-character string (format XXXXXXXX-XXXXXXXXXXXXXXXX) that uniquely identifies a specific iPhone, iPad, Apple Watch, Apple TV, Vision Pro or Mac.</p>
<p>There are several ways to find a device's UDID:</p>
<ul>
<li><p><strong>Xcode:</strong> Connect the device to a Mac running Xcode. Open Xcode and navigate to Window &gt; Devices and Simulators. Select the connected device from the list on the left. The UDID will be displayed as the "Identifier" in the device information panel.</p>
</li>
<li><p><strong>Finder (macOS Catalina and later):</strong> Connect the iOS or iPadOS device to a Mac. Open Finder and select the device from the sidebar under "Locations." The UDID may be displayed directly, or it might be necessary to click on the line of text beneath the device's name (which shows model, storage, and OS version) to cycle through to display the UDID.</p>
</li>
<li><p><strong>iTunes (older macOS versions):</strong> For Macs running macOS Mojave or earlier, connect the device and open iTunes. Select the device icon when it appears. In the "Summary" tab, click on the "Serial Number" field; this will change to display the UDID.</p>
</li>
<li><p><strong>Apple Silicon Macs:</strong> When registering an Apple Silicon Mac, it's important to look for the "Provisioning UDID," which can be found in System Information under Hardware &gt; Provisioning UDID.</p>
</li>
<li><p><strong>Other Ways:</strong> There are some websites that will install a profile on to your device to get the UUID – so as an absolute last resort, you can do this. <em>But I highly recommend doing it in the one of the official ways to avoid any potential issues.</em></p>
</li>
</ul>
<h3 id="heading-how-to-register-devices-in-the-apple-developer-portal">How to Register Devices in the Apple Developer Portal</h3>
<p>Device registration is managed through the "Certificates, Identifiers &amp; Profiles" section of the Apple Developer Portal (developer.apple.com) and typically requires an Account Holder or Admin role.</p>
<p>To manually register a single device:</p>
<ol>
<li><p>Sign in to the Apple Developer Portal and navigate to "Certificates, Identifiers &amp; Profiles," then select "Devices" from the sidebar.</p>
</li>
<li><p>Click the add button (+) to register a new device.</p>
</li>
<li><p>Select the correct platform for the device (for example, iOS, macOS, tvOS, watchOS).</p>
</li>
<li><p>Enter a descriptive "Device Name" (this is for your reference, for example, "Sravan’s iPhone 11 Pro") and the device's UDID obtained in the previous step.</p>
</li>
<li><p>Click "Continue," review the information to make sure everything is correct, and then click "Register".</p>
</li>
</ol>
<p>For registering multiple devices, the portal supports uploading a specially formatted text file (a .txt or a .deviceids file) containing device names and UDIDs.</p>
<p>If "Automatically manage signing" is enabled in Xcode, Xcode can automatically register a connected device when it's selected as a build target. This is the way I managed all of my personal projects and devices. On the other hand, the file upload was really useful at my workplace to keep track of all the devices and add them at once.</p>
<h3 id="heading-understanding-device-limits-and-annual-resets">Understanding Device Limits and Annual Resets</h3>
<p>The Apple Developer Program imposes limits on the number of devices that can be registered for testing:</p>
<ul>
<li><p><strong>Annual Limit:</strong> Each membership year, a development team can register up to 100 devices for each product family (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro, Mac). If you are a large team, this can potentially bottleneck you. When we ran into this issue, we created a new development team that could be split so that it didn’t have too much interdependence. There is no other way as far as I know, other than asking Apple and appealing them.</p>
</li>
<li><p><strong>Disabling Devices:</strong> While a device can be disabled in the portal during the membership year, doing so <strong>does not free up its slot or increase the number of available devices for that year</strong>. This part is frustrating but I think this is the only way they can enforce the 100 device limit to avoid people swapping devices. They should just provide a pathway to increase the limit, really. Disabling a device will, however, invalidate any provisioning profiles that include it, requiring those profiles to be regenerated.</p>
</li>
<li><p><strong>Resetting Device List (Start of New Membership Year):</strong> At the beginning of a new membership year, Account Holders, Admins, and App Managers are given a one-time option when they first sign in to "Certificates, Identifiers &amp; Profiles" to remove devices from their list. This allows them to "reset" their available device count back to 100 for each product family. You can choose to remove specific devices or all registered devices. <strong>This is your one chance per year to remove unused devices completely and free up slots for new devices.</strong></p>
</li>
<li><p><strong>Membership Expiration:</strong> If a developer program membership is nearing expiration and is not planned for renewal, the Account Holder will have an option, starting 30 days before expiration, to download a copy of their registered device list. They can also opt to have all devices removed from the account immediately upon membership expiration. If no action is taken, devices are typically removed automatically 180 days after membership expiration.</p>
</li>
</ul>
<h2 id="heading-possibilities-enabling-capabilities-and-services"><strong>Possibilities: Enabling Capabilities and Services</strong></h2>
<p>App Capabilities (or App Services) are features provided by Apple that we (as developers) can integrate into our applications to extend functionality and provide richer user experiences. Examples include iCloud storage, Push Notifications, Sign in with Apple, Apple Pay and HealthKit integration. Enabling these often requires explicit configuration for an app's App ID in the Apple Developer Portal and within the Xcode project.</p>
<h3 id="heading-why-you-should-use-capabilities">Why You Should Use Capabilities</h3>
<p>Making full use of these App capabilities can set your app apart from other apps in a very noticeable way. You can use Apple Wallet integration if you want users to scan a membership card. You can use journaling suggestions if you want to prompt them to journal something. You can use iCloud Storage to lean further into inter-device synchronization.</p>
<p>When you enable a capability for an App ID, it results in specific entitlements being added to the app's provisioning profile. These entitlements are permissions that the operating system checks at runtime to ensure the app is authorized to use the requested service.</p>
<h3 id="heading-how-to-configure-capabilities-for-your-app-id-apple-developer-portal">How to Configure Capabilities for Your App ID (Apple Developer Portal)</h3>
<p>Enabling and configuring capabilities is typically done by an Account Holder or Admin in the Apple Developer Portal (developer.apple.com).</p>
<ol>
<li><p>Navigate to "Certificates, Identifiers &amp; Profiles" and select "Identifiers."</p>
</li>
<li><p>Choose the App ID for which capabilities need to be configured.</p>
</li>
<li><p>In the App ID's settings, there will be a "Capabilities" tab. Select the checkboxes for the capabilities the app requires.</p>
</li>
<li><p>Many capabilities require additional configuration steps. For these, a "Configure" or "Edit" button will usually appear next to the capability once selected. Examples include:</p>
</li>
</ol>
<ul>
<li><p><strong>App Groups:</strong> Requires creating or selecting an app group identifier to allow data sharing between a main app and its extensions, or between different apps from the same developer.</p>
</li>
<li><p><strong>Apple Pay:</strong> Requires associating one or more Merchant IDs with the App ID.</p>
</li>
<li><p><strong>iCloud:</strong> May require choosing Xcode version compatibility and creating or assigning iCloud containers for Key-Value or Document storage</p>
</li>
<li><p><strong>Sign in with Apple:</strong> May require configuring the App ID as a primary app or grouping it with an existing primary App ID, and optionally providing a server-to-server notification endpoint URL.</p>
</li>
</ul>
<ol start="5">
<li>After configuring all selected capabilities, click "Save." A warning dialog may appear, which needs confirmation to finalize the changes.</li>
</ol>
<p><strong>Enabling a capability in the Developer Portal is only one part of the process.</strong> You’ll also need to add and configure it within the app's target in the Xcode project, under the "Signing &amp; Capabilities" tab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748480139418/6a4007b3-01bd-484a-865c-8c5e728e15e0.png" alt="Showing the Signing &amp; Capabilities screen in Xcode" class="image--center mx-auto" width="614" height="114" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748480260906/e0dcec33-24ce-448b-91be-b79f5638e6fc.png" alt="Screenshot showing the Capabilities selector in Xcode" class="image--center mx-auto" width="793" height="608" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748480340624/ac56896a-0fb0-4cb0-a3fc-c894a255794a.png" alt="Screenshot of Xcode showing three capabilities. " class="image--center mx-auto" width="1187" height="434" loading="lazy"></p>
<ol>
<li><p>Navigate to the project settings and select “Signing &amp; Capabilities”.</p>
</li>
<li><p>Press the “+ Capability” button to select the capability.</p>
</li>
<li><p>Once selected, the capability should appear in the pane. Depending on the capability, you might want to configure it further.</p>
</li>
</ol>
<p>This Xcode step integrates the necessary frameworks, adds entitlements files to the project, and adjusts build settings.</p>
<h3 id="heading-how-enabling-capabilities-affects-your-provisioning-profiles">How Enabling Capabilities Affects Your Provisioning Profiles</h3>
<p>Changes to an App ID's enabled capabilities have a direct and significant impact on its associated provisioning profiles.</p>
<ul>
<li><p><strong>Invalidation:</strong> When a capability is enabled, disabled, or its configuration is modified for an App ID, <strong>all existing provisioning profiles that use that App ID immediately become invalid</strong>.</p>
</li>
<li><p><strong>Regeneration Required:</strong> These invalidated provisioning profiles must be regenerated (either by editing and re-saving them in the Developer Portal or by having Xcode's automatic signing handle it). The regenerated profiles will then include the updated set of entitlements corresponding to the newly configured capabilities.</p>
</li>
<li><p><strong>Platform Impact:</strong> Enabling a capability for an App ID that is used across multiple platforms (for example, an iOS app and its watchOS companion) will affect the provisioning profiles for all eligible platforms that use that App ID.</p>
</li>
</ul>
<p>This is something to keep in mind. Especially when it comes to distribution profiles since those are usually manually managed.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>While all of these might seem daunting, Apple’s automatic process should handle most of it. But I highly recommend learning how everything works so that you can debug it in case something goes wrong. I also highly recommend using manually created profiles for distribution.</p>
<p>While signing and handling certificates is not the most exciting part of the App development process, it is a necessary skill to have. In my next article, I will go over distributing an app from start to finish (which includes these processes and more restrictions).</p>
<p>You can follow me at <a class="user-mention" href="https://hashnode.com/@sravankaruturi">Sravan Karuturi</a> for my other posts.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
