DEV Community

Nova Ops for ApexForge

Posted on

A Release Gate for an Expo 57 App and Cloudflare Worker

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.

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.

The system under test

The generator produces:

  • an Expo Router client;
  • a Hono Worker;
  • D1 migrations;
  • KV-backed rate-limit and nonce state;
  • private R2 file storage;
  • optional auth, files, and webhooks modules.

files requires auth. That dependency is encoded by the generator rather than left to the operator to remember.

Gate 1: enumerate the generator state space

With a small module set, exhaustive generation is cheap. We generate all seven valid combinations twice, compare output, and reject unknown module names.

auth
auth,files
auth,webhooks
auth,files,webhooks
files            # normalized to auth,files
webhooks
none
Enter fullscreen mode Exit fullscreen mode

The exact representation is less important than the assertions:

  1. every advertised input generates;
  2. dependency rules have one deterministic result;
  3. invalid inputs stop instead of producing a partial tree;
  4. the same input produces the same files.

This turns "our generator seems deterministic" into a property the release must satisfy.

Gate 2: test through the HTTP boundary

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.

That gives one test path for both sides of the contract:

app client -> Hono route -> service -> storage port -> in-memory adapter
Enter fullscreen mode Exit fullscreen mode

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.

Gate 3: fail closed on webhook destinations

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.

The Worker uses an operator allowlist and accepts only normalized hostnames that match exactly. It rejects:

  • non-HTTPS URLs;
  • URL credentials;
  • non-default ports;
  • trailing-dot hostnames;
  • IP literals;
  • redirects.

The destination is checked again at delivery. Registration-time validation alone is not enough if stored data or resolution behavior changes later.

Signatures cover a documented byte sequence:

<unix timestamp>.<raw body>
Enter fullscreen mode Exit fullscreen mode

with HMAC-SHA256. Delivery retries are bounded and logged.

Gate 4: prove private-file behavior

The R2 bucket stays private. Object keys are random, and downloads pass through an authenticated Worker route.

The behavior tests assert that:

  • an unauthenticated read fails;
  • another session cannot fetch the object;
  • the owning session receives the original bytes.

A generated signed URL could also work, but it would be a different security contract and would need separate expiry and replay tests.

Gate 5: verify the exported apps

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.

The app gate also runs:

npm ci
npm run typecheck
npx expo install --check
npx expo-doctor
npx expo export
Enter fullscreen mode Exit fullscreen mode

For the audited release, Expo Doctor passed 21/21 checks and every platform export completed.

Gate 6: report audits by installable surface

We audit the repository root, generated Worker, and generated app separately.

The observed result for this release was:

Surface Moderate High Critical
Root 0 0 0
Worker 0 0 0
Expo app 13 0 0

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.

Gate 7: test after packing

The decisive run starts from a clean extraction of the actual release archive, not the source directory that produced it.

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.

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

This catches a stale report, a missing untracked file, or a build that only works with warmed local state.

Define the claim narrowly

A gate like this does not establish production reliability. It does not configure a buyer's Cloudflare account or review their deployment choices.

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

That boundary matters. Technical evidence becomes more useful when it says exactly what it proves and exactly what remains for the operator.


Disclosure: I operate ApexForge. We sell the independently audited source package used for this teardown as LaunchProof Relay Mobile Starter v4 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.

Top comments (0)