> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/expo-open-ota/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mercure-technologies.gitbook.io/expo-open-ota/stateless-mode/local-development.md).

# Local development

{% hint style="info" %}
This page only concerns projects that use `expo-dev-client`. Without it, debug builds fetch their bundle straight from Metro and never request a manifest, so code signing is never exercised in development and you can skip this page.
{% endhint %}

In stateless mode you generated the code signing key pair yourself with `npx eoas generate-certs`. The certificate, `certificate.pem`, is committed with your app so builds can verify update signatures, while the key pair is a server-side secret that belongs in your deployment configuration (see [Key Store](/expo-open-ota/key-store/keys.md)).\
\
This page explains how your whole team can run the development server without ever touching that key.

## When the problem appears

In development, an `expo-dev-client` build does not load its JavaScript blindly: the launcher first asks the dev server for a manifest, through expo-updates, using the configuration embedded in the build. Because `updates.codeSigningCertificate` is set in your Expo config, that embedded configuration includes your certificate, so the request demands a signed manifest. The Expo CLI must then sign it with a private key, which it only reads from the `--private-key-path` flag, and without it the command fails:

```shellscript
CommandError: Must specify --private-key-path argument to sign development manifest for requested code signing key
```

The historical workaround was to generate a separate development key pair, embed its certificate in development builds and start the server with `npx expo start --dev-client --private-key-path ./certs/private-key-dev.pem`. It works, but it multiplies key material and per-environment configuration for no benefit: development builds never verify production updates, so there is nothing to sign in the first place. The cleaner fix is to not embed any code signing configuration in development builds. Note that `eoas init` also adds `private-key.pem` to your `.gitignore`, because the production key has no business inside the app repository at all.

## Disable code signing in development

Recent versions of `eoas init` write the condition into your `app.config.js` for you. Make sure yours guards the two code signing fields on `DISABLE_CODE_SIGNING`:

```javascript
export default ({ config }) => ({
  ...config,
  updates: {
    url: 'https://your-server.com/manifest',
    enabled: true,
    requestHeaders: {
      'expo-channel-name': process.env.RELEASE_CHANNEL,
      'expo-app-id': 'your-app-id',
    },
    codeSigningCertificate: process.env.DISABLE_CODE_SIGNING ? undefined : './certs/certificate.pem',
    codeSigningMetadata: process.env.DISABLE_CODE_SIGNING
      ? undefined
      : { keyid: 'main', alg: 'rsa-v1_5-sha256' },
  },
});
```

Signing stays on unless explicitly disabled: forgetting the variable breaks a development command noisily, while a production build can never silently lose signature verification.

{% hint style="warning" %}
Never set `DISABLE_CODE_SIGNING` when building a release. The variable acts at prebuild time: it decides whether the certificate gets embedded in the binary. A release build made with it, wherever you build it (locally with `--configuration Release`, in your CI, or on a build service), would apply updates from your server without verifying their signature, and nothing would warn you. Keep the variable in your dev scripts or a gitignored `.env.local` only, never in a committed `.env` or in your release build environment.
{% endhint %}

If you build with EAS Build, you can make that mistake impossible there. EAS sets `EAS_BUILD=true` on its workers, so with this fuse above the export a misconfigured profile fails the build loudly instead of shipping without signature verification:

```js
if (process.env.DISABLE_CODE_SIGNING && process.env.EAS_BUILD) {
  throw new Error('DISABLE_CODE_SIGNING must never be set on an EAS build');
}
```

## Run the development server

The variable matters on both sides. At prebuild time it decides whether the build embeds the certificate, and therefore whether the app will ask for signed manifests. When starting the dev server it decides whether the CLI would try to sign them. A build made with the variable must be served by a dev server started with it, so keep them consistent by putting it in the scripts everyone uses:

```json
{
  "scripts": {
    "start_dev": "DISABLE_CODE_SIGNING=1 expo start --dev-client",
    "ios_dev": "DISABLE_CODE_SIGNING=1 expo run:ios",
    "android_dev": "DISABLE_CODE_SIGNING=1 expo run:android"
  }
}
```

{% hint style="info" %}
If you already generated your native projects with code signing enabled, run `DISABLE_CODE_SIGNING=1 npx expo prebuild --clean` once so development builds stop embedding the certificate.
{% endhint %}

Production builds are unaffected: built without the variable, they embed the certificate configured during [Getting Started](/expo-open-ota/stateless-mode/getting-started.md) and verify the signature of every update they download.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mercure-technologies.gitbook.io/expo-open-ota/stateless-mode/local-development.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
