open-wa
DocsManaging Secrets in Config

Managing Secrets in Config

Load API keys and integration secrets explicitly through environment variables and pluginConfig.

Keep secrets outside committed source, load them into process.env before the config file is evaluated, and pass each value through the config key that actually consumes it. The examples target @open-wa/wa-automate@5.1.0 and use WA_* names for runtime settings.

Load a .env file explicitly

The open-wa config loader reads process.env; it does not make a project .env file appear automatically. Create a local npm project and install the runtime, the webhook plugin referenced below, and a .env loader:

npm init -y
npm install @open-wa/wa-automate@5.1.0 @open-wa/integration-webhook@5.1.0 dotenv

Create .env next to wa.config.mjs and keep it out of version control:

WA_API_KEY=replace-with-a-long-random-key
WA_SESSION_ID=sales
WA_PORT=8080
OPEN_WA_WEBHOOK_SECRET=replace-with-webhook-secret
CHATWOOT_URL=https://chatwoot.example/app/accounts/1/inboxes/2
CHATWOOT_API_ACCESS_TOKEN=replace-with-chatwoot-token
OPEN_WA_PUBLIC_URL=https://openwa-api.example

Load the file in wa.config.mjs, then start the supported release with that config:

// wa.config.mjs
import 'dotenv/config';

const required = (name) => {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
};

export default {
  apiKey: required('WA_API_KEY'),
  sessionId: process.env.WA_SESSION_ID ?? 'sales',
  port: Number(process.env.WA_PORT ?? 8080),
  plugins: ['@open-wa/integration-webhook'],
  pluginConfig: {
    webhook: {
      url: 'https://your-app.example/webhooks/open-wa',
      events: ['message.received'],
      headers: {
        'X-Webhook-Secret': required('OPEN_WA_WEBHOOK_SECRET'),
      },
    },
  },
};
npx wa-automate --config ./wa.config.mjs --session-id sales --host 127.0.0.1 --port 8080

The shared config loader reads WA_API_KEY, WA_PORT, and WA_SESSION_ID. The published 5.1.0 CLI applies its own session, host, and port defaults after that loader, so the command supplies those three flags explicitly. OPEN_WA_WEBHOOK_SECRET and CHATWOOT_API_ACCESS_TOKEN are application-level names; the config file maps them to the integration settings that consume them.

Docker secrets and environment variables

For complete, digest-pinned run and Compose commands, see the Docker guide. Compose reads .env values for interpolation but passes only mapped environment values into the container. Set W_A_V=5.1.0 there to pin the package installed at startup.

The container does not read Docker secret files by itself. If your platform mounts a secret under /run/secrets, use an entrypoint that reads that file, exports its value as WA_API_KEY, and then runs the open-wa CLI. Restrict access to both the secret file and the session profile.

Plugin configuration shape

The top-level plugins array contains npm package names or local plugin paths. The pluginConfig object is keyed by each plugin's meta.name; the plugin validates and consumes the value under its own key.

Webhook integration

Webhook delivery uses the @open-wa/integration-webhook plugin. Its supported config is an object under pluginConfig.webhook, with url, optional events, and optional request headers:

export default {
  plugins: ['@open-wa/integration-webhook'],
  pluginConfig: {
    webhook: {
      url: 'https://your-app.example/webhooks/open-wa',
      events: ['message.received'],
      headers: {
        'X-Webhook-Secret': process.env.OPEN_WA_WEBHOOK_SECRET,
      },
    },
  },
};

The top-level webhook config key is a separate string URL field. The current v5 CLI does not register delivery from --webhook; use the plugin shape above for webhook delivery and see Webhook payloads for the receiver contract.

Chatwoot integration

Chatwoot uses the plugin's documented field names, not instanceUrl:

Install the plugin package into the same npm project that installs the runtime:

npm install @open-wa/integration-chatwoot@5.1.0
export default {
  plugins: ['@open-wa/integration-chatwoot'],
  pluginConfig: {
    chatwoot: {
      chatwootUrl: process.env.CHATWOOT_URL,
      chatwootApiAccessToken: process.env.CHATWOOT_API_ACCESS_TOKEN,
      apiHost: process.env.OPEN_WA_PUBLIC_URL,
      apiKey: process.env.WA_API_KEY,
      forceUpdateCwWebhook: true,
    },
  },
};

chatwootUrl contains the Chatwoot account URL and optional inbox path, chatwootApiAccessToken authenticates API calls, and apiHost is the public origin Chatwoot can reach. See Chatwoot integration for the schema and webhook route.

Illustrative custom plugin

my-plugin is an invented application example. Its fields have no meaning to open-wa unless your plugin defines and reads them:

export default {
  plugins: ['./plugins/my-plugin.mjs'],
  pluginConfig: {
    'my-plugin': {
      apiKey: process.env.MY_PLUGIN_API_KEY,
      enabled: true,
    },
  },
};

Define and validate those fields in the plugin's own configSchema; do not copy this shape into a real integration without checking that integration's schema.

Security rules

  • Use environment variables or a secret manager for API keys, license keys, webhook secrets, and integration tokens.
  • Add .env and local secret directories to .gitignore; never commit session profiles or secret files.
  • Never log the complete config object. Log only booleans such as hasKey: Boolean(config.apiKey) and non-sensitive URLs.
  • Rotate a secret by changing the injected value and restarting the runtime or plugin so it is loaded again.
  • Treat the browser profile directory as secret material because it contains WhatsApp authentication state.

Validate at startup

Validate required application secrets before starting the runtime. Zod is one option for an application-owned config layer:

import { z } from 'zod';

const configSchema = z.object({
  apiKey: z.string().min(1),
  webhookSecret: z.string().min(1),
});

const appConfig = configSchema.parse({
  apiKey: process.env.WA_API_KEY,
  webhookSecret: process.env.OPEN_WA_WEBHOOK_SECRET,
});

The runtime validates its own config and plugin schemas separately. Application validation prevents a missing secret from becoming a partially configured integration.

Was this helpful?

Your answer includes the page path and docs version.

On this page