<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fonchu</title>
    <description>The latest articles on DEV Community by Fonchu (@fonchu).</description>
    <link>https://dev.to/fonchu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F807258%2F0de26c64-ce12-4e22-b907-b0396717e49a.png</url>
      <title>DEV Community: Fonchu</title>
      <link>https://dev.to/fonchu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fonchu"/>
    <language>en</language>
    <item>
      <title>Tenant Isolation at the API Edge: Per-Tenant API Keys, Usage Plans and Throttling on Amazon API Gateway</title>
      <dc:creator>Fonchu</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:38:07 +0000</pubDate>
      <link>https://dev.to/fonchu/tenant-isolation-at-the-api-edge-per-tenant-api-keys-usage-plans-and-throttling-on-amazon-api-2h20</link>
      <guid>https://dev.to/fonchu/tenant-isolation-at-the-api-edge-per-tenant-api-keys-usage-plans-and-throttling-on-amazon-api-2h20</guid>
      <description>&lt;p&gt;&lt;em&gt;A worked example: a multi-company employee directory, built with AWS CDK, Amazon Cognito and React.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When every customer of a multi-tenant API shares one rate limit, &lt;strong&gt;one busy customer can degrade service for all the others&lt;/strong&gt;. This is the &lt;em&gt;noisy neighbour&lt;/em&gt; problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amazon API Gateway solves it at the edge, with no code in the request path: issue &lt;strong&gt;one API key per tenant&lt;/strong&gt;, attach each key to a &lt;strong&gt;usage plan per pricing tier&lt;/strong&gt;, and API Gateway enforces the plan's limits &lt;strong&gt;for each key independently&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API keys identify a tenant for throttling and metering; they are &lt;strong&gt;not an authentication mechanism&lt;/strong&gt;. Authenticate with &lt;strong&gt;Amazon Cognito&lt;/strong&gt;, carry the tenant in an &lt;strong&gt;immutable JWT claim&lt;/strong&gt;, and &lt;strong&gt;verify that the API key belongs to that tenant&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate isolation is not enough when the data is personal. &lt;strong&gt;Scope every read to the tenant named in the token.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Changing a tenant's tier means &lt;strong&gt;moving its key to another plan&lt;/strong&gt;, not editing the plan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Plan for the service limits early: &lt;strong&gt;10,000 API keys per account per Region, a quota that cannot be increased&lt;/strong&gt;, and the &lt;strong&gt;Lambda concurrency&lt;/strong&gt; behind the API, which can cap traffic before any usage plan does.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design uses one API key per company and one usage plan per tier, and adds key-to-tenant binding, data isolation, tier changes, key rotation, tenant suspension, and usage reporting. A deployable sample accompanies the post on &lt;a href="https://github.com/vernyuy/apigateway-tenant-throttling" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;: AWS CDK infrastructure and a React platform console with a traffic lab.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The scenario: a multi-company employee directory
&lt;/h2&gt;

&lt;p&gt;Consider a SaaS product that hosts employee directories for many companies. Each customer company has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Employees&lt;/strong&gt; who sign in to look up colleagues: name, title, department, and email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integrations&lt;/strong&gt;, such as an HR system that synchronises the directory or an intranet page that searches it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;subscription tier&lt;/strong&gt;: Basic for small teams, Premium for growing companies, Enterprise for large organisations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In multi-tenant terms, &lt;strong&gt;each company is a tenant&lt;/strong&gt;. The platform owes every company two guarantees:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data isolation.&lt;/strong&gt; One company must never see another company's employees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance isolation.&lt;/strong&gt; One company's traffic must never degrade another company's service.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post focuses on the second guarantee and shows how it fits together with the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The problem with a shared rate limit
&lt;/h2&gt;

&lt;p&gt;A typical first version of the API has a single throttle on the API Gateway stage, for example 20 requests per second. That limit is shared by &lt;strong&gt;every company combined&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now one large customer runs a nightly HR synchronisation that pages through its entire directory as fast as it can. It consumes the shared allowance, and employees at every other company receive &lt;code&gt;429 Too Many Requests&lt;/code&gt; when they search for a colleague. The platform also cannot answer basic operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How much is each company actually using?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can one misbehaving integration be switched off without affecting anyone else?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can customers on a higher tier be offered a higher limit?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. How API Gateway throttling works
&lt;/h2&gt;

&lt;p&gt;API Gateway throttles with a &lt;strong&gt;token bucket&lt;/strong&gt;: tokens are added at the configured &lt;em&gt;rate&lt;/em&gt; (requests per second), and the bucket holds at most &lt;em&gt;burst&lt;/em&gt; tokens. Each request consumes one token; when the bucket is empty, the request is rejected with a 429.&lt;/p&gt;

&lt;p&gt;Limits are applied in four layers, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-client limits from a usage plan&lt;/strong&gt; (per API key), or per-method limits within a plan&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Per-method limits configured on the stage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The account-level limit per Region (by default 10,000 requests per second with a burst of 5,000; the rate can be raised)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS Regional throttling&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first layer is the one that matters for tenant isolation. The AWS documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Throttling and quota limits apply to requests for individual API keys that are aggregated across all API stages within a usage plan."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a usage plan allows 10 requests per second and ten companies' keys belong to it, &lt;strong&gt;each company receives 10 requests per second&lt;/strong&gt;. They do not share a single allowance.&lt;/p&gt;

&lt;p&gt;One caveat, also from the documentation: throttles and quotas are "applied on a best-effort basis and should be thought of as targets rather than guaranteed request ceilings." Usage plans are the right tool for fairness and tiered pricing. For hard abuse protection or cost control, add AWS WAF rate-based rules and AWS Budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 A worked example
&lt;/h3&gt;

&lt;p&gt;The sample's Basic plan has a rate of &lt;strong&gt;2 requests per second&lt;/strong&gt; and a burst of &lt;strong&gt;4&lt;/strong&gt;. Picture its bucket:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It holds at most 4 tokens, and it is full when the company has been idle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every request takes one token. A request that finds the bucket empty receives a 429.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tokens are added back at 2 per second.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a Basic company can briefly exceed 2 requests per second, until its saved tokens run out. After that it gets 2 per second, however much it sends:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;A Basic company sends&lt;/th&gt;
&lt;th&gt;Expected in the first second&lt;/th&gt;
&lt;th&gt;Expected every second after that&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 requests per second&lt;/td&gt;
&lt;td&gt;About 4–5 succeed (the bucket was full)&lt;/td&gt;
&lt;td&gt;About &lt;strong&gt;2 succeed, 3 throttled&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 requests per second&lt;/td&gt;
&lt;td&gt;About 4–6 succeed in theory; often more in practice (see below)&lt;/td&gt;
&lt;td&gt;About &lt;strong&gt;2 succeed, 8 throttled&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Users do not matter, keys do.&lt;/strong&gt; All of a company's employees share its key. Ten employees sending one request per second each are indistinguishable from one integration sending ten: the company has 10 requests per second against a limit of 2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only the noisy company is affected.&lt;/strong&gt; A Premium company calling at the same moment still has its own 10 requests per second.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the first case measured with the sample's traffic lab (section 9). For 20 seconds, Acme Robotics (Basic) sends 5 requests per second while Initech Labs (Premium) also sends 5:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f5znqjee5o92dgwgmyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f5znqjee5o92dgwgmyu.png" alt="Traffic lab: a Basic company at 5 requests per second is held to about 2 successful requests per second, while a Premium company at the same rate is unaffected" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Acme Robotics' first two seconds are fully green while the saved tokens last. After that it settles at about 2 successful requests per second, with the rest throttled: 43 successful and 57 throttled in total, 2.15 per second on average. Initech Labs is not affected: all 100 of its requests succeed.&lt;/p&gt;

&lt;p&gt;Doubling Acme Robotics to 10 requests per second throttles more requests, but the successful rate barely moves (46 successful, 154 throttled, 2.3 per second):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ql4jxodj5imo7xp8gii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ql4jxodj5imo7xp8gii.png" alt="Traffic lab: at 10 requests per second the Basic company still gets about 2 successful requests per second" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the first two seconds: 10 and 8 requests succeeded, more than a 4-token bucket suggests. This is the best-effort behaviour described above. Over any period longer than a few seconds, the limit holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Architecture
&lt;/h2&gt;

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

&lt;p&gt;Every request from a company's directory app passes three independent checks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answered by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Who is calling, and for which company?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API Gateway's built-in Cognito authorizer validates the JWT; its &lt;code&gt;custom:tenant_id&lt;/code&gt; claim names the company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Which plan, and how fast?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API Gateway validates &lt;code&gt;x-api-key&lt;/code&gt; and applies that key's usage plan, returning 429 when it is exceeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Does the key belong to the caller's company?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The directory Lambda compares the request's API key ID with the company's stored key ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fourth rule governs the data itself: &lt;strong&gt;every read is scoped to the company in the token.&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;sequenceDiagram&lt;/span&gt;
    &lt;span class="nx"&gt;autonumber&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Company&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;C&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Amazon&lt;/span&gt; &lt;span class="nx"&gt;Cognito&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;GW&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;Gateway&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;UP&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Usage&lt;/span&gt; &lt;span class="nf"&gt;plan &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;per&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Directory&lt;/span&gt; &lt;span class="nx"&gt;Lambda&lt;/span&gt;
    &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="nx"&gt;DB&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;DynamoDB&lt;/span&gt;

    &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Employee&lt;/span&gt; &lt;span class="nx"&gt;signs&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ID&lt;/span&gt; &lt;span class="nf"&gt;token &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ID&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
    &lt;span class="nx"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cognito&lt;/span&gt; &lt;span class="nx"&gt;authorizer&lt;/span&gt; &lt;span class="nx"&gt;validates&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;JWT&lt;/span&gt;
    &lt;span class="nx"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Valid&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;Tokens&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="nx"&gt;alt&lt;/span&gt; &lt;span class="nx"&gt;Over&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s rate or quota
        UP--&amp;gt;&amp;gt;App: 429 Too Many Requests
    else Unknown or disabled key
        UP--&amp;gt;&amp;gt;App: 403 Forbidden
    else Within limits
        GW-&amp;gt;&amp;gt;L: Invoke (identity.apiKeyId, claims)
        L-&amp;gt;&amp;gt;DB: Load company record
        alt Key does not belong to the token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;
            &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="nx"&gt;Forbidden&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Query&lt;/span&gt; &lt;span class="nx"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s company only
            L--&amp;gt;&amp;gt;App: 200 Employee directory
        end
    end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both gateway checks are performed by API Gateway itself. &lt;strong&gt;No Lambda function runs in front of the throttle&lt;/strong&gt;, so rejected and throttled requests cost nothing beyond API Gateway. If you use a Lambda authorizer instead, verify whether it runs before the API key check: if it does, a flooding tenant invokes it on every request, and a WAF rate-based rule in front becomes worthwhile.&lt;/p&gt;

&lt;p&gt;Platform administration (onboarding companies, changing tiers, rotating and suspending keys, reading usage) is a separate set of &lt;code&gt;/admin/*&lt;/code&gt; routes that call the API Gateway management APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Design choice: a key per company, or a key per tier?
&lt;/h2&gt;

&lt;p&gt;The AWS Partner Network post referenced above also discusses the main alternative: &lt;strong&gt;one API key per tier, shared by every tenant on that tier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With a shared key, tenants never hold a key. The API's key source is set to &lt;code&gt;AUTHORIZER&lt;/code&gt;, and a Lambda authorizer returns the tier's key. Per the AWS documentation, the authorizer "must return one of the usage plan's API keys as the &lt;code&gt;usageIdentifierKey&lt;/code&gt; property value":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Shared-key variant, shown for comparison (not used by the sample)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTHORIZER&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Inside a Lambda authorizer, after verifying the JWT:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policyDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;usageIdentifierKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TIER_KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;tierOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom:tenant_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built-in Cognito authorizer cannot return a key, so this variant requires a Lambda authorizer that validates the JWT itself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;One key per company (this sample)&lt;/th&gt;
&lt;th&gt;One key per tier (shared)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Noisy-neighbour isolation&lt;/td&gt;
&lt;td&gt;Each company has its own bucket&lt;/td&gt;
&lt;td&gt;Companies on the same tier share a bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota&lt;/td&gt;
&lt;td&gt;Per company&lt;/td&gt;
&lt;td&gt;Shared across the tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage reporting (&lt;code&gt;GetUsage&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Per company&lt;/td&gt;
&lt;td&gt;Per tier only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suspending one company at the gateway&lt;/td&gt;
&lt;td&gt;Disable its key&lt;/td&gt;
&lt;td&gt;Not possible; must be enforced in the authorizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of keys&lt;/td&gt;
&lt;td&gt;One per company, up to 10,000 per account and Region (hard limit)&lt;/td&gt;
&lt;td&gt;One per tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;CreateApiKey&lt;/code&gt; call (5 per second per account)&lt;/td&gt;
&lt;td&gt;A database write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier change&lt;/td&gt;
&lt;td&gt;Move the key between plans; can take minutes&lt;/td&gt;
&lt;td&gt;Immediate, through the authorizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorizer&lt;/td&gt;
&lt;td&gt;Built-in Cognito authorizer&lt;/td&gt;
&lt;td&gt;Lambda authorizer on every uncached request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The AWS post summarises the trade-off: a shared key means fewer keys to manage, but "the solution is more susceptible to noisy neighbor effect." Because performance isolation is the goal here, the sample uses &lt;strong&gt;one key per company&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Shared tier keys remain a sound choice for free or trial tiers with many small tenants, where isolation matters less than conserving the 10,000-key budget. A &lt;strong&gt;hybrid&lt;/strong&gt; is common: a shared key for the free tier and dedicated keys for paid tiers. Because an API has a single key source, a hybrid uses &lt;code&gt;AUTHORIZER&lt;/code&gt;, which means the authorizer must know the dedicated keys' values (kept in AWS Secrets Manager) rather than only their IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Implementation
&lt;/h2&gt;

&lt;p&gt;The code below is taken from the sample. Paths are relative to &lt;code&gt;infra/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Tiers as configuration
&lt;/h3&gt;

&lt;p&gt;All limits live in one place. Each value applies &lt;strong&gt;per company key&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/tiers.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;basic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="na"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DAY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;premium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="na"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DAY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;enterprise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DAY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STAGE_THROTTLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;   &lt;span class="c1"&gt;// all companies combined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample's limits are deliberately small so that throttling is visible from a browser. Production values would typically be an order of magnitude higher.&lt;/p&gt;

&lt;p&gt;The console reads these values from the deployed stack, so what it shows always matches what API Gateway enforces:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs9vsqil5sibb0d3fammj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs9vsqil5sibb0d3fammj.png" alt="The console's Plans table: Basic 2 requests per second, Premium 10, Enterprise 50, and the stage limit of 100 for all companies combined" width="799" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 One usage plan per tier, and a stage ceiling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/tenant-api-stack.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// clients send x-api-key&lt;/span&gt;
  &lt;span class="na"&gt;deployOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;throttlingRateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STAGE_THROTTLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;throttlingBurstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STAGE_THROTTLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quota&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TIERS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addUsagePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;UsagePlan`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;usagePlanName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                          &lt;span class="c1"&gt;// applied to each key separately&lt;/span&gt;
    &lt;span class="na"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;apiStages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deploymentStage&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Size the stage limit against what you sell.&lt;/strong&gt; One hundred Premium companies at 10 requests per second can send 1,000 requests per second between them. If the stage allows less, the shared stage bucket becomes the bottleneck and the noisy-neighbour problem returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Identity: Amazon Cognito with an immutable tenant claim
&lt;/h3&gt;

&lt;p&gt;AWS is explicit that API keys are not an authentication mechanism:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Don't use API keys for authentication or authorization to control access to your APIs."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Employees authenticate with Amazon Cognito. Each user carries a &lt;code&gt;tenant_id&lt;/code&gt; custom attribute, which Cognito includes in the ID token as the &lt;code&gt;custom:tenant_id&lt;/code&gt; claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UserPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TenantUsers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;featurePlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FeaturePlan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;selfSignUpEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customAttributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringAttribute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mutable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attribute must be &lt;strong&gt;immutable&lt;/strong&gt;. If users could change it, they could move themselves into another company.&lt;/p&gt;

&lt;p&gt;For server-to-server integrations such as an HR system sync, the same model works with the OAuth 2.0 client credentials grant: one Cognito app client per company, with the company identified by the token's &lt;code&gt;client_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Requiring a JWT and an API key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MethodOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AuthorizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COGNITO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authorizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CognitoUserPoolsAuthorizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TenantAuthorizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cognitoUserPools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userPool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resultsCacheTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;apiKeyRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;employees&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;employeesFn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;tenantAuth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A method does not need two authorizers to enforce both checks. &lt;code&gt;apiKeyRequired&lt;/code&gt; is a method setting that API Gateway enforces alongside the authorizer.&lt;/p&gt;

&lt;p&gt;A request from a company's directory app therefore carries both credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /employees
Authorization: eyJraWQiOi...      (Cognito ID token)
x-api-key: 9vKq2n...              (the company's API key)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.5 Binding the key to the company
&lt;/h3&gt;

&lt;p&gt;API Gateway confirms that the key is &lt;strong&gt;valid&lt;/strong&gt;. It does not know &lt;strong&gt;whose&lt;/strong&gt; it is. Without one more check, an employee of company A could send company B's key and consume B's allowance.&lt;/p&gt;

&lt;p&gt;The Lambda receives the verified key ID from API Gateway, so the check is a single lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/employees.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom:tenant_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// validated JWT claim&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                         &lt;span class="c1"&gt;// from the key check&lt;/span&gt;

&lt;span class="c1"&gt;// Both reads are scoped to the token's company, so they run in parallel.&lt;/span&gt;
&lt;span class="c1"&gt;// Nothing is returned until the key check has passed.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;getTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;listEmployees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key does not belong to the caller's company&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the two reads in parallel is not only about latency. Every request occupies a Lambda execution for as long as it runs, and section 8 shows why that time matters.&lt;/p&gt;

&lt;p&gt;Store the &lt;strong&gt;key ID&lt;/strong&gt;, never the key value. The ID is what the request context provides, and it is of no use to an attacker.&lt;/p&gt;

&lt;p&gt;A useful consequence: a company's API key only works together with a valid token from the same company. A leaked key cannot be used by anyone outside that company.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.6 Data isolation: scope every read to the token's company
&lt;/h3&gt;

&lt;p&gt;A directory holds personal data, so performance isolation must sit on top of strict data isolation. The employee table is partitioned by company, and the company always comes from the validated token, never from the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/lib/employees.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listEmployees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMPLOYEES_TABLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;KeyConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenantId = :tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="c1"&gt;// optional department filter ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No query parameter, path segment or header can select another company. For defence in depth at scale, consider DynamoDB fine-grained access control with the &lt;code&gt;dynamodb:LeadingKeys&lt;/code&gt; condition, so that the function's credentials can only read the caller's partition.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.7 Onboarding a company
&lt;/h3&gt;

&lt;p&gt;Onboarding creates the company's first Cognito user, issues an API key into its tier's plan, seeds its directory and writes the company record. If any step fails, the earlier steps are rolled back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/lib/api-keys.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issueApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usagePlanId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;planIdFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateApiKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`tenant-demo-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// the name is chosen by the platform&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// the value is generated by API Gateway&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateUsagePlanKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;usagePlanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revokeApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// never leave an orphaned key&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A new key is not usable immediately.&lt;/strong&gt; In the sample, requests with a newly created key were rejected with a 403 (&lt;code&gt;INVALID_API_KEY&lt;/code&gt; in the access log) for about 45–65 seconds, even though the key was already in its plan. Tell new customers to expect this, or have onboarding wait until a test call succeeds.&lt;/p&gt;

&lt;p&gt;A key's &lt;strong&gt;name&lt;/strong&gt; and its &lt;strong&gt;value&lt;/strong&gt; are distinct. The platform chooses the name. API Gateway generates the value, as AWS recommends, and the value is returned to the company &lt;strong&gt;once&lt;/strong&gt;. The company record stores only &lt;code&gt;apiKeyId&lt;/code&gt; and the last four characters for display.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.8 Pitfall: the CloudFormation dependency cycle
&lt;/h3&gt;

&lt;p&gt;The natural approach is to pass each usage plan's ID to the admin Lambda as an environment variable. CloudFormation rejects it, because the references form a cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda → usage plan → stage → deployment → method → Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, pass the plan &lt;strong&gt;names&lt;/strong&gt;, which are plain strings, and resolve the IDs at runtime, caching them per execution environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;USAGE_PLAN_NAMES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usagePlanNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An SSM parameter written by the stack is an equally valid alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the plan names unique in the account.&lt;/strong&gt; Plan names are not unique in API Gateway, and the lookup takes the first plan with a matching name. If a second copy of the stack is deployed, for example under a new stack name, both copies have a plan called &lt;code&gt;basic&lt;/code&gt;. A new company's key can then be added to the &lt;em&gt;other&lt;/em&gt; API's plan, and every request it makes is rejected with a 403. The sample prefixes each plan name with the stack name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/tiers.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usagePlanName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.9 Upgrades and downgrades
&lt;/h3&gt;

&lt;p&gt;A usage plan's limits apply to &lt;strong&gt;every&lt;/strong&gt; company on that tier, so editing a plan to upgrade one company would upgrade them all. Instead, move the company's key to the target tier's plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/lib/api-keys.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syncTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;planIdFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetUsagePlansCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;currentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeleteUsagePlanKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;usagePlanId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateUsagePlanKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;usagePlanId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The key does not change.&lt;/strong&gt; The company's integrations keep working without reconfiguration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remove before adding.&lt;/strong&gt; "A given API key can only be associated with one usage plan for each stage of your API."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Changes are not instant.&lt;/strong&gt; After a key is added to a usage plan, "the update operation might take a few minutes to complete." Schedule tier changes accordingly, or inform the customer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make it idempotent.&lt;/strong&gt; &lt;code&gt;syncTier&lt;/code&gt; converges on the desired state, so a failed change can simply be retried. The handler updates the company record first, then synchronises the key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a single code path.&lt;/strong&gt; If the tier is edited directly in the database, the key remains in its old plan.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.10 Rotation and suspension
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rotation&lt;/strong&gt; issues a new key, points the company at it with a conditional write and then deletes the old key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;issueApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;last4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revokeApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conditional write (&lt;code&gt;apiKeyId = :previous&lt;/code&gt;) prevents two concurrent rotations from both succeeding. If deleting the old key fails, the key is still harmless: it no longer matches the company record, so the binding check rejects it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspension&lt;/strong&gt; disables the key. API Gateway then rejects it with a 403 before any application code runs, and only that company is affected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpdateApiKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;patchOperations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.11 Usage reporting and observability
&lt;/h3&gt;

&lt;p&gt;API Gateway meters usage per key. &lt;code&gt;GetUsage&lt;/code&gt; returns a daily &lt;code&gt;[used, remaining]&lt;/code&gt; pair against the plan's quota:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetUsageCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;usagePlanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is sufficient for a usage page, alerting or usage-based billing. API Gateway meters usage but does not bill; invoicing remains the platform's responsibility.&lt;/p&gt;

&lt;p&gt;For request-level visibility, include the key and user in the stage's access logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;accessLogFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AccessLogFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$context.requestId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$context.status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$context.identity.apiKeyId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// identifies the company&lt;/span&gt;
  &lt;span class="na"&gt;userSub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$context.authorizer.claims.sub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// identifies the employee&lt;/span&gt;
  &lt;span class="na"&gt;errorType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$context.error.responseType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// THROTTLED, QUOTA_EXCEEDED, INVALID_API_KEY, ...&lt;/span&gt;
&lt;span class="p"&gt;})),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identifying which company is being throttled then becomes a CloudWatch Logs Insights query.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.12 Pitfall: CORS on gateway responses
&lt;/h3&gt;

&lt;p&gt;When a browser calls the API, responses generated by API Gateway itself, such as a 429 or 403, carry &lt;strong&gt;no CORS headers&lt;/strong&gt; by default. The browser then hides the status and reports an opaque network error. Add the headers to the gateway responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGatewayResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default4xx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_4XX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;responseHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Best practices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Issue one API key per tenant and one usage plan per tier. Per-key limits are what contain noisy neighbours.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep the stage limit, and the account limit, above the combined allowance you sell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use per-method throttles within a plan for expensive operations, such as a full directory export.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add AWS WAF rate-based rules for abuse protection. Usage plans are best-effort targets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remember that limits apply per tenant, not per user. Within a company, users share the company's allowance.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Never treat an API key as a credential. Pair it with Cognito, IAM or OAuth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Carry the tenant in an immutable claim, and bind the API key to that tenant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scope every data access to the tenant in the token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Let API Gateway generate keys, store only key IDs, and show key values once.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Change tiers by moving keys between plans, through one idempotent code path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expect plan membership changes to take minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log &lt;code&gt;apiKeyId&lt;/code&gt; on every request, and use &lt;code&gt;GetUsage&lt;/code&gt; for per-tenant metering.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keys created at runtime are not stack resources. Offboard tenants before deleting the stack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grant the admin function least-privilege access to the specific API Gateway paths it manages.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Service limits to plan around
&lt;/h2&gt;

&lt;p&gt;From the &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html" rel="noopener noreferrer"&gt;API Gateway quotas&lt;/a&gt; documentation at the time of writing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quota&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Adjustable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API keys per account per Region&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage plans per account per Region&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage plans per API key&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method-level throttling settings per stage in a usage plan&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Yes, through AWS Support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account throttle per Region&lt;/td&gt;
&lt;td&gt;10,000 requests/s, burst 5,000&lt;/td&gt;
&lt;td&gt;Rate only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CreateApiKey&lt;/code&gt; and &lt;code&gt;DeleteApiKey&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5 requests/s per account&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UpdateUsagePlan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 request every 20 s per account&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda concurrent executions per Region (all functions)&lt;/td&gt;
&lt;td&gt;1,000; new accounts can start much lower, for example 10&lt;/td&gt;
&lt;td&gt;Yes, through Service Quotas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Implications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;10,000 tenants per account and Region is a hard ceiling&lt;/strong&gt; for the per-key pattern. Beyond it, use a cell-based architecture with tenants distributed across accounts or Regions, or move the smallest tenants to shared tier keys (section 5).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onboarding is rate-limited.&lt;/strong&gt; Bulk imports need a queue with retries. The sample returns 429 when &lt;code&gt;CreateApiKey&lt;/code&gt; is throttled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never edit a plan to change one tenant.&lt;/strong&gt; Besides affecting every tenant on the tier, &lt;code&gt;UpdateUsagePlan&lt;/code&gt; is limited to one call every 20 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda concurrency is a shared ceiling behind the API.&lt;/strong&gt; Every successful request runs the directory function, and the account can only run a limited number of executions at once. Above that, Lambda rejects the call and API Gateway returns a &lt;strong&gt;5xx&lt;/strong&gt;, not a 429, for every tenant alike. An account limited to 10 concurrent executions served roughly 50 requests per second in the sample before errors appeared. Check the limit with &lt;code&gt;aws lambda get-account-settings&lt;/code&gt;, raise it before launch, and keep functions on the request path short: the sample gives the directory function 1,024 MB (more memory also means more CPU, so faster cold starts) and runs its reads in parallel.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Running the sample
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;           &lt;span class="c"&gt;# CDK assertion tests, no AWS calls&lt;/span&gt;
npm run deploy     &lt;span class="c"&gt;# deploys the stack and writes web/.env.local&lt;/span&gt;
npm run web        &lt;span class="c"&gt;# starts the console on http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Retrieve the admin token using the &lt;code&gt;AdminTokenCommand&lt;/code&gt; stack output, open the console and connect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On &lt;strong&gt;Companies&lt;/strong&gt;, select &lt;strong&gt;New company&lt;/strong&gt; and create &lt;strong&gt;Acme Robotics&lt;/strong&gt; on Basic and &lt;strong&gt;Initech Labs&lt;/strong&gt; on Premium. Each receives a Cognito user, an API key and a sample directory. The credentials are shown once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8skkwl3gdy63hzxyebe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8skkwl3gdy63hzxyebe.png" alt="The New company dialog, with Acme Robotics on the Basic tier" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait about a minute for the new keys to become active (section 6.7). Meanwhile, open a company's detail panel and its &lt;strong&gt;Directory&lt;/strong&gt; tab. Each company sees only its own employees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the &lt;strong&gt;Traffic lab&lt;/strong&gt; and set both companies to 5 requests per second for 20 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Few7sqs3sfufjlvhb3780.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Few7sqs3sfufjlvhb3780.png" alt="Traffic lab controls: each company's rate and limit, presets relative to each company's limit, the run duration, and the combined total" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Each company's own limit is shown next to its tier.

*   **Every company at** sets all companies to a multiple of their own limit: ½×, 1×, 2× or 5×.

*   The total line compares the combined rate with the two ceilings shared by all companies: the stage limit, and an estimate of what the account's Lambda concurrency can serve.


Select **Run for 20s**. The lab first sends one request per company to sign in and warm up the function, then starts the clock. The result should look like the first chart in section 3.1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Set Acme Robotics to 10 requests per second and run again. The successful rate stays at about 2 per second; only the throttled count grows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Change Acme Robotics to Premium in its detail panel, allow a few minutes for the change to take effect, and run again with the same key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Suspend Initech Labs' key. API Gateway rejects every request with a 403.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the &lt;strong&gt;Usage&lt;/strong&gt; tab to view a company's daily request count; the &lt;strong&gt;Overview&lt;/strong&gt; page totals today's requests across all companies.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the combined rate goes above what the account's Lambda concurrency can serve, the lab warns that extra requests will fail with 5xx errors rather than 429s, and offers to scale every company down proportionally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddi5cpwyytb1vmvzu81f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddi5cpwyytb1vmvzu81f.png" alt="Traffic lab warning: 80 requests per second in total is above the roughly 50 that this account's Lambda limit can serve" width="799" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The traffic lab simulates a &lt;strong&gt;rate per company&lt;/strong&gt;, not a population of users. Each company signs in as a single user and sends evenly spaced requests from the browser. Because limits apply per key, this produces the same throttling as many users sharing the key at the same total rate.&lt;/p&gt;

&lt;p&gt;To clean up, delete the companies in the console (this removes their API keys, Cognito users and directories), then run &lt;code&gt;npm run destroy --workspace=infra&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Conclusion
&lt;/h2&gt;

&lt;p&gt;Per-tenant API keys and per-tier usage plans give a multi-tenant API effective performance isolation without adding code to the request path. API Gateway enforces the limits, and the platform gains per-tenant quotas, per-tenant suspension and per-tenant usage data.&lt;/p&gt;

&lt;p&gt;The design decisions that matter most are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;keep authentication separate from the API key;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;bind each key to the tenant in the token;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;scope every data access to that tenant;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;treat tiers as plans that keys move between;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;plan for the 10,000-key ceiling before it is reached;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;size what sits behind the gateway, such as Lambda concurrency, for the traffic the plans allow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/apn/enabling-tiering-and-throttling-in-a-multi-tenant-amazon-eks-saas-solution-using-amazon-api-gateway/" rel="noopener noreferrer"&gt;Enabling Tiering and Throttling in a Multi-Tenant Amazon EKS SaaS Solution Using Amazon API Gateway (AWS Partner Network Blog)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" rel="noopener noreferrer"&gt;Usage plans and API keys for REST APIs in API Gateway&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html" rel="noopener noreferrer"&gt;Throttle requests to your REST APIs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-key-source.html" rel="noopener noreferrer"&gt;Choose an API key source in API Gateway&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html" rel="noopener noreferrer"&gt;Output from an API Gateway Lambda authorizer&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html" rel="noopener noreferrer"&gt;Amazon API Gateway quotas&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html" rel="noopener noreferrer"&gt;Quotas for configuring and running a REST API&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/api/API_GetUsage.html" rel="noopener noreferrer"&gt;GetUsage API reference&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html" rel="noopener noreferrer"&gt;Lambda function scaling and concurrency&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>multitenancy</category>
      <category>apigateway</category>
      <category>isolation</category>
    </item>
    <item>
      <title>Understanding Single Table Database Design in DynamoDB</title>
      <dc:creator>Fonchu</dc:creator>
      <pubDate>Wed, 12 Jul 2023 19:07:06 +0000</pubDate>
      <link>https://dev.to/fonchu/understanding-single-table-database-design-in-dynamodb-2f89</link>
      <guid>https://dev.to/fonchu/understanding-single-table-database-design-in-dynamodb-2f89</guid>
      <description>&lt;p&gt;Single table design is a powerful approach to database modeling in DynamoDB. It involves storing all of your data in a single table, which simplifies the data model and reduces the number of tables needed to store data. This approach is simple, efficient, and flexible, allowing for seamless scalability and high performance. With single table design, you can structure your data as needed and add new attributes to the table without requiring schema changes. Ultimately, this approach makes it easier to manage and maintain your database, while also improving query times and reducing costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importance/Advantages of single table design
&lt;/h2&gt;

&lt;p&gt;Single table design is an important aspect of DynamoDB database modeling. Here are some reasons why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: Single table design simplifies the data model and reduces the number of tables needed to store data. This makes it easier to manage and maintain the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency&lt;/strong&gt;: Single table design reduces the number of read and write operations needed to access data. This can result in faster query times and lower costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Single table design allows for seamless scalability. As more data is added to the table, it can be partitioned across multiple physical storage partitions, ensuring that the table can handle large amounts of data without performance degradation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Single table design allows for flexible data modeling. Data can be structured as needed, and new attributes can be added to the table without requiring schema changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High performance&lt;/strong&gt;: Single table design can lead to high performance and low latency. This is because all data is stored in a single table, reducing the need for joins and other operations that can slow down query times.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Disadvantages of using sing table design.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Increased complexity in query logic&lt;/strong&gt;: With a single table design, it may be more difficult to construct queries that retrieve only specific subsets of data. This can lead to increased complexity in query logic and potentially slower query times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Possible performance issues with high write throughput&lt;/strong&gt;: If the table has a high write throughput, the single table design may not be able to keep up with the volume of write requests. This could result in performance issues and slower write times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficulty managing access control&lt;/strong&gt;: With a single table design, managing access control can become more complex, particularly if different users or applications require different levels of access to different parts of the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increased potential for data errors&lt;/strong&gt;: With a single table design, it may be easier to accidentally overwrite or delete data when performing updates. This could result in data errors and inconsistencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited flexibility in data modeling&lt;/strong&gt;: While the single table design does offer some flexibility in data modeling, it may be more difficult to change the structure of the table once data has been added. This could limit the ability to adapt the data model to changing requirements over time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Consider the following when building a single table design
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition Key&lt;/strong&gt;: A Partition key is part of the primary key that determines the partition in which an item is stored&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sort Key SK&lt;/strong&gt;: A sort key is part of the primary key that allows you to sort items within a partition key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Global Secondary Index GSI&lt;/strong&gt;. A GSI is a way to create an alternate sort key for a DynamoDB table, allowing you to perform queries using attributes other than the primary key. It includes a partition key and a sort key, and can be created at any time after the table has been created. By projecting attributes from the table into the index, you can retrieve data from the index without having to access the table itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Primary Key&lt;/strong&gt;: The primary key is made up of two parts: the partition key and the sort key. The partition key is used to partition data across multiple nodes in the DynamoDB cluster, while the sort key is used to sort the items within each partition.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Steps involve when a creating a single table design
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entities&lt;/strong&gt;: Identify all entities and their attributes and &lt;em&gt;note&lt;/em&gt; the primary key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationship&lt;/strong&gt; Map the relationships between the entities.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Generate a single table
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the primary key&lt;/strong&gt;: The primary key is the main attribute used to access items in the table. It can be a simple or composite key, and should be designed to support the most common access patterns for the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determine the attributes&lt;/strong&gt;: Determine the attributes that will be stored in the table. These should include all of the data needed for the primary access patterns, as well as any additional data needed for other access patterns or queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define secondary indexes&lt;/strong&gt;: Secondary indexes can be used to support additional access patterns or queries that are not well-suited to the primary key. Consider which secondary indexes will be needed and how they will be used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determine the data types&lt;/strong&gt;: Choose appropriate data types for each attribute in the table, based on the type of data being stored and the access patterns being used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider data normalization&lt;/strong&gt;: Consider whether to normalize the data in the table to reduce redundancy and improve data consistency. This can be particularly useful for tables with complex access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define access patterns&lt;/strong&gt;: Define the access patterns that will be used to retrieve data from the table. These should be based on the most common queries and should take into account the primary key and any secondary indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for scalability&lt;/strong&gt;: Plan for how the table will scale as the amount of data grows. Consider how partitioning will be used to distribute the data across multiple physical storage partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define access controls&lt;/strong&gt;: Define the access controls that will be used to restrict access to the table. This should include defining roles and permissions for users and applications that will access the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test and iterate&lt;/strong&gt;: Test the table design and iterate as needed to ensure that it meets the performance and scalability requirements of the application.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Use Case
&lt;/h2&gt;

&lt;p&gt;Single table design in DynamoDB is best suited for use cases where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data access patterns are well-defined and can be modeled with a single partition key and sort key&lt;/li&gt;
&lt;li&gt;The data can be effectively modeled with a single table, without requiring complex relationships between tables&lt;/li&gt;
&lt;li&gt;The data needs to be retrieved or updated frequently, and performance is a major concern&lt;/li&gt;
&lt;li&gt;The data needs to be easily scalable, with the ability to handle large amounts of data and high levels of traffic&lt;/li&gt;
&lt;li&gt;The data can be effectively partitioned across multiple physical storage partitions, allowing for seamless scalability and high performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, single table design is a powerful approach to database modeling in DynamoDB that can simplify the data model, improve efficiency, allow for seamless scalability, provide flexibility, and lead to high performance. However, it may not be the best choice for all use cases, and developers should carefully consider the potential advantages and disadvantages before deciding whether to use a single table design for their application.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
