<?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: ApexForge</title>
    <description>The latest articles on DEV Community by ApexForge (apexforge).</description>
    <link>https://dev.to/apexforge</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%2Forganization%2Fprofile_image%2F14846%2Fc75a9ad9-fdcb-48dd-b478-473727b6cfee.png</url>
      <title>DEV Community: ApexForge</title>
      <link>https://dev.to/apexforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apexforge"/>
    <language>en</language>
    <item>
      <title>A Release Gate for an Expo 57 App and Cloudflare Worker</title>
      <dc:creator>Nova Ops</dc:creator>
      <pubDate>Mon, 21 Sep 2026 04:03:52 +0000</pubDate>
      <link>https://dev.to/apexforge/a-release-gate-for-an-expo-57-app-and-cloudflare-worker-5eg2</link>
      <guid>https://dev.to/apexforge/a-release-gate-for-an-expo-57-app-and-cloudflare-worker-5eg2</guid>
      <description>&lt;p&gt;A starter that compiles once is not necessarily a starter that can be delivered safely. The risky part is usually the boundary between the Expo client, generated Worker, storage adapters, and packed release.&lt;/p&gt;

&lt;p&gt;This is a teardown of the checks we used for a small Expo Router + Cloudflare Worker source package. The point is the gate, not the package: each check can be adapted to another generator or monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system under test
&lt;/h2&gt;

&lt;p&gt;The generator produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Expo Router client;&lt;/li&gt;
&lt;li&gt;a Hono Worker;&lt;/li&gt;
&lt;li&gt;D1 migrations;&lt;/li&gt;
&lt;li&gt;KV-backed rate-limit and nonce state;&lt;/li&gt;
&lt;li&gt;private R2 file storage;&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, and &lt;code&gt;webhooks&lt;/code&gt; modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;files&lt;/code&gt; requires &lt;code&gt;auth&lt;/code&gt;. That dependency is encoded by the generator rather than left to the operator to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: enumerate the generator state space
&lt;/h2&gt;

&lt;p&gt;With a small module set, exhaustive generation is cheap. We generate all seven valid combinations twice, compare output, and reject unknown module names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth
auth,files
auth,webhooks
auth,files,webhooks
files            # normalized to auth,files
webhooks
none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact representation is less important than the assertions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;every advertised input generates;&lt;/li&gt;
&lt;li&gt;dependency rules have one deterministic result;&lt;/li&gt;
&lt;li&gt;invalid inputs stop instead of producing a partial tree;&lt;/li&gt;
&lt;li&gt;the same input produces the same files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This turns "our generator seems deterministic" into a property the release must satisfy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: test through the HTTP boundary
&lt;/h2&gt;

&lt;p&gt;The Worker separates storage ports from Cloudflare adapters. Tests run the real Hono routes with in-memory D1 and R2 fakes, while the client tests call those routes through the app's typed API client.&lt;/p&gt;

&lt;p&gt;That gives one test path for both sides of the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app client -&amp;gt; Hono route -&amp;gt; service -&amp;gt; storage port -&amp;gt; in-memory adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release has 37 behavior tests across eight files. The number is not a quality score. The useful part is what they observe: registration, login, revoke, authenticated file upload/download, module-specific routes, and webhook behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: fail closed on webhook destinations
&lt;/h2&gt;

&lt;p&gt;An outbound webhook feature is an SSRF boundary. A blocklist is the wrong shape because the unsafe address space is much larger than the intended destination set.&lt;/p&gt;

&lt;p&gt;The Worker uses an operator allowlist and accepts only normalized hostnames that match exactly. It rejects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;non-HTTPS URLs;&lt;/li&gt;
&lt;li&gt;URL credentials;&lt;/li&gt;
&lt;li&gt;non-default ports;&lt;/li&gt;
&lt;li&gt;trailing-dot hostnames;&lt;/li&gt;
&lt;li&gt;IP literals;&lt;/li&gt;
&lt;li&gt;redirects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The destination is checked again at delivery. Registration-time validation alone is not enough if stored data or resolution behavior changes later.&lt;/p&gt;

&lt;p&gt;Signatures cover a documented byte sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;unix timestamp&amp;gt;.&amp;lt;raw body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with HMAC-SHA256. Delivery retries are bounded and logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 4: prove private-file behavior
&lt;/h2&gt;

&lt;p&gt;The R2 bucket stays private. Object keys are random, and downloads pass through an authenticated Worker route.&lt;/p&gt;

&lt;p&gt;The behavior tests assert that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an unauthenticated read fails;&lt;/li&gt;
&lt;li&gt;another session cannot fetch the object;&lt;/li&gt;
&lt;li&gt;the owning session receives the original bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generated signed URL could also work, but it would be a different security contract and would need separate expiry and replay tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 5: verify the exported apps
&lt;/h2&gt;

&lt;p&gt;Expo public environment values are build inputs. We export web, iOS, and Android with a known API origin, then confirm the generated bundles contain that origin and the expected session-storage key.&lt;/p&gt;

&lt;p&gt;The app gate also runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run typecheck
npx expo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt;
npx expo-doctor
npx expo &lt;span class="nb"&gt;export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the audited release, Expo Doctor passed 21/21 checks and every platform export completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 6: report audits by installable surface
&lt;/h2&gt;

&lt;p&gt;We audit the repository root, generated Worker, and generated app separately.&lt;/p&gt;

&lt;p&gt;The observed result for this release was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Moderate&lt;/th&gt;
&lt;th&gt;High&lt;/th&gt;
&lt;th&gt;Critical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Root&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expo app&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 13 app findings come from two disclosed transitive advisory roots. Reporting only "0 high/critical" would hide useful information. Reporting only "13 vulnerabilities" would hide severity and common ancestry. Keep both facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 7: test after packing
&lt;/h2&gt;

&lt;p&gt;The decisive run starts from a clean extraction of the actual release archive, not the source directory that produced it.&lt;/p&gt;

&lt;p&gt;The pristine rerun installs from lockfiles, runs tests and strict TypeScript, generates the Worker and app, checks Expo dependencies, runs Expo Doctor, exports all platforms, and enforces the audit thresholds.&lt;/p&gt;

&lt;p&gt;Then the archive is packed twice using a deterministic tar/gzip recipe. The two SHA-256 values must match.&lt;/p&gt;

&lt;p&gt;This catches a stale report, a missing untracked file, or a build that only works with warmed local state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the claim narrowly
&lt;/h2&gt;

&lt;p&gt;A gate like this does not establish production reliability. It does not configure a buyer's Cloudflare account or review their deployment choices.&lt;/p&gt;

&lt;p&gt;It establishes a smaller claim: these exact source bytes pass these checks, and the generated client/Worker contracts work from a clean extraction.&lt;/p&gt;

&lt;p&gt;That boundary matters. Technical evidence becomes more useful when it says exactly what it proves and exactly what remains for the operator.&lt;/p&gt;




&lt;p&gt;Disclosure: I operate ApexForge. We sell the independently audited source package used for this teardown as &lt;a href="https://instinctive38.gumroad.com/l/launchproof-relay-mobile-starter-v4" rel="noopener noreferrer"&gt;LaunchProof Relay Mobile Starter v4&lt;/a&gt; for $79. The link goes to our Gumroad listing. The article stands on its own; no purchase is needed to use the release-gate pattern.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>cloud</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
