> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modulex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Data security & encryption

> How ModuleX encrypts your integration credentials and secrets at rest, isolates each credential with its own key, keeps API keys one-way and unrecoverable, sources its master secrets from your environment or a managed key vault, and refuses to start in production without strong keys in place.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

ModuleX stores the secrets you trust it with — integration access tokens, API keys, OAuth client secrets — **encrypted at rest**, and never returns them in plaintext once written. Each credential is protected with its own key, and your ModuleX API keys are kept only as one-way fingerprints that can be verified but never read back. This page explains what ModuleX protects, how those protections work at a high level, and the production safeguards that keep the underlying keys strong.

For how credentials are modelled, resolved at run time, and exposed through the credentials API, see [Credentials & OAuth2](/concepts/credentials-oauth). For the two authentication paths into ModuleX, see [Auth model: JWT vs API key](/security/authentication).

<Note>
  The operator-facing settings on this page — the master secrets, the managed key vault, and the production startup checks — apply to **self-hosted and managed deployments**. On the hosted ModuleX service these are handled for you, and you never set them yourself.
</Note>

## What ModuleX protects

<MediaEmbed id="MX-MEDIA-4250" type="image" caption={"What ModuleX protects and how it is kept safe"} />

<CardGroup cols={3}>
  <Card title="Connected credentials" icon="lock">
    Access tokens, API keys, and OAuth settings for the services you connect are encrypted before they are stored, and decrypted only in memory at the moment of use.
  </Card>

  <Card title="OAuth app secrets" icon="key">
    The secrets behind ModuleX's managed OAuth apps — the ones that let you connect a service without registering your own OAuth application — are protected with their own separately-keyed encryption.
  </Card>

  <Card title="Your ModuleX API keys" icon="fingerprint">
    API keys are never stored. ModuleX keeps a one-way fingerprint, shows you the full key once, and can verify it later without ever holding the original.
  </Card>
</CardGroup>

### Per-credential isolation

Every credential is sealed with a key that is **unique to that credential in that organization**. Because the key is bound to both the organization and the specific credential, a stored secret cannot be unlocked outside the organization it belongs to — and a leaked record on its own is not enough to read a secret, because the master key is required as well.

### API keys are one-way

When you create a ModuleX API key, ModuleX generates it from strong randomness, shows it to you **once**, and keeps only a one-way fingerprint hardened with a server-side secret that lives outside the database. On every request the presented key is checked against that fingerprint in a way that resists timing attacks. Because the original is never stored, a key cannot be shown again after creation — if one leaks, revoke it and issue a new one. For the key format and how keys authenticate requests, see [Authentication](/api-reference/authentication).

## Secrets are never returned in plaintext

Once a credential is saved, ModuleX never returns its secret. List and detail responses show a masked value — only the first and last few characters, for example `start***end` — and OAuth2 credentials report a literal `OAuth2` label rather than any token. ModuleX also redacts sensitive fields such as passwords, tokens, and API keys from application logs, so there is no API or log path that emits a stored secret in full.

```json theme={null}
{
  "credential_id": "b3f1...uuid",
  "integration_name": "github",
  "auth_type": "oauth2",
  "auth_data_masked": "OAuth2",
  "is_default": true
}
```

## Key management

The encryption that protects your credentials depends on two master secrets. On self-hosted and managed deployments, these resolve from your **environment first, then a managed key vault** — so you can keep them out of source control and in the secret store you already trust.

<ParamField path="ENCRYPTION_KEY" type="string" required>
  The master key behind all credential encryption.

  * **Minimum length:** 32 characters.
  * **Production / staging:** required (see the production safeguards below).
  * **Generate:** `python3 -c "import secrets; print(secrets.token_urlsafe(32))"`

  <Note>
    Treat `ENCRYPTION_KEY` as **permanent for the life of your data** — it is the key that unlocks every stored credential, so keep it stable and store it safely.
  </Note>
</ParamField>

<ParamField path="API_KEY_PEPPER" type="string" required>
  The server-side secret that hardens the one-way fingerprints of your ModuleX API keys.

  * **Minimum length:** 32 characters.
  * **Production / staging:** required.
  * **Generate:** `python3 -c "import secrets; print(secrets.token_hex(32))"`

  <Note>
    Like `ENCRYPTION_KEY`, set `API_KEY_PEPPER` once, keep it stable, and set it explicitly when you self-host.
  </Note>
</ParamField>

### Sourcing secrets from a key vault

When `AZURE_KEY_VAULT_URL` is set, ModuleX sources its master secrets from **Azure Key Vault** instead of reading them from the environment. The client authenticates with a managed identity, so there is no secret-to-fetch-secrets bootstrap problem. Secrets are read once at startup, so plan rotations around a restart.

<ParamField path="AZURE_KEY_VAULT_URL" type="string">
  The Key Vault URL (for example `https://my-vault.vault.azure.net/`). Setting it enables vault sourcing; leaving it unset means ModuleX reads from environment variables only — the default for local development.
</ParamField>

<CodeGroup>
  ```bash Environment variables theme={null}
  # Local / env-only: set the master secrets directly.
  export ENCRYPTION_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
  export API_KEY_PEPPER="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
  export ENVIRONMENT=production
  ```

  ```bash Azure Key Vault theme={null}
  # Production: source the same secrets from Key Vault instead of the environment.
  export AZURE_KEY_VAULT_URL="https://my-vault.vault.azure.net/"
  export ENVIRONMENT=production
  ```
</CodeGroup>

## Production safeguards

ModuleX validates its security configuration at startup. In **production and staging**, a missing or weak `ENCRYPTION_KEY` or `API_KEY_PEPPER` stops the backend from starting at all — there is no degraded mode, so a misconfigured deployment refuses to serve traffic rather than run with weak keys. When the checks pass, the backend continues normally.

<ParamField path="ENVIRONMENT" type="string" default="development">
  The deployment environment. `production` and `staging` turn on the strict startup checks above.
</ParamField>

<Warning>
  If a deployment exits immediately at startup with a security-validation message, set `ENCRYPTION_KEY` and `API_KEY_PEPPER` to strong 32+ character values (or wire up a key vault) and restart. Do not switch `ENVIRONMENT` away from `production` to get past it — that turns off the protection that keeps your stored secrets safe.
</Warning>

## Compliance

For ModuleX's compliance posture, certifications, and the list of sub-processors, see [Compliance](/security/compliance) and [Sub-processors](/security/sub-processors).

## Related

<CardGroup cols={2}>
  <Card title="Credentials & OAuth2" icon="key" href="/concepts/credentials-oauth">
    How credentials are modelled, resolved at run time, and exposed through the credentials API — including the OAuth2 PKCE flow.
  </Card>

  <Card title="Auth model: JWT vs API key" icon="shield-check" href="/security/authentication">
    The two authentication paths into ModuleX and the headers each request must carry.
  </Card>

  <Card title="Security overview" icon="lock" href="/security/overview">
    How ModuleX secures your data, credentials, and access across the platform.
  </Card>

  <Card title="Deployment & SSO" icon="server" href="/enterprise/deployment-sso">
    Single sign-on and self-hosted or managed deployment options for enterprise.
  </Card>
</CardGroup>
