mastra-ai/mastra · error

MastraFactory: integration '${integration.id}' signs OAuth s

Error message

MastraFactory: integration '${integration.id}' signs OAuth state and requires a replica-stable state secret, but none is configured. Set 'stateSecret' on the factory config.

What it means

Integrations that sign OAuth 'state' parameters require a replica-stable secret: a per-process random signer breaks the OAuth callback on any replica that did not sign the state. During prepare(), if an active integration sets requiresStableStateSigner and the configured state signer is not stable (no stateSecret configured), the factory fails at boot rather than at first OAuth flow.

Source

Thrown at mastracode/factory/src/factory.ts:603

                  ),
              }
            : {}),
          messageReader: {
            listMessages: async input => {
              const memory = await storage.getMastraStorage().getStore('memory');
              return memory ? memory.listMessages(input) : { messages: [], hasMore: false };
            },
          },
        })
      : undefined;

    // Boot assertion: an active integration that signs OAuth `state` needs a
    // replica-stable signer — a per-process random secret silently breaks the
    // OAuth callback on any replica that didn't sign the state. Fail loud now
    // instead. (The built-ins also assert this inside their readiness gates.)
    for (const { integration } of integrationRegistrations) {
      if (integration.requiresStableStateSigner && !stateSigner.stable) {
        throw new Error(
          `MastraFactory: integration '${integration.id}' signs OAuth state and requires a ` +
            `replica-stable state secret, but none is configured. Set 'stateSecret' on the factory config.`,
        );
      }
    }

    // The SDK needs to know which backend the injected Mastra store uses
    // (its own `instanceof` detection breaks when the dependency graph holds
    // duplicate package copies). Resolve it by walking the FactoryStorage
    // prototype chain by class name — the factory can't import the concrete
    // classes since '@mastra/pg' / '@mastra/libsql' are the user's choice.
    const mastraStorageBackend = (() => {
      for (let proto = Object.getPrototypeOf(storage); proto; proto = Object.getPrototypeOf(proto)) {
        if (proto.constructor?.name === 'PgFactoryStorage') return 'pg' as const;
        if (proto.constructor?.name === 'LibSQLFactoryStorage') return 'libsql' as const;
      }
      return undefined;
    })();

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Set stateSecret in the factory config: stateSecret: process.env.FACTORY_STATE_SECRET, sourced from a stable secret store.
  2. Provision the same secret across all replicas/instances so any replica can verify state signed by another.
  3. Add the env var to your deployment platform's secret manager and restart all instances after adding it.
  4. For local dev, generate one long random value and commit it to .env (gitignored) so dev behavior matches production.

Example fix

// before
const factory = new MastraFactory({
  storage,
  integrations: [new GithubIntegration()],
});

// after
const factory = new MastraFactory({
  storage,
  stateSecret: process.env.FACTORY_STATE_SECRET,
  integrations: [new GithubIntegration()],
});
Defensive patterns

Strategy: validation

Validate before calling

if (!process.env.FACTORY_STATE_SECRET || process.env.FACTORY_STATE_SECRET.length < 32) {
  throw new Error('FACTORY_STATE_SECRET must be set (32+ chars) for integrations that sign OAuth state');
}
const config = { ...baseConfig, stateSecret: process.env.FACTORY_STATE_SECRET };

Type guard

function hasStableStateSecret(config) {
  return typeof config.stateSecret === 'string' && config.stateSecret.length >= 32;
}
if (!hasStableStateSecret(factoryConfig)) throw new Error('Set a replica-stable stateSecret before boot');

Try / catch

try {
  await factory.prepare();
} catch (err) {
  if (err.message.includes('replica-stable state secret')) {
    throw new Error('Deploy config error: FACTORY_STATE_SECRET missing — OAuth will break across replicas', { cause: err });
  }
  throw err;
}

Prevention

When it happens

Trigger: Configuring an integration (e.g. GithubIntegration) whose requiresStableStateSigner is true, while the factory config lacks stateSecret — so stateSigner.stable is false at boot.

Common situations: Deploying behind multiple replicas/instances without a shared secret; local dev working because one process signs and verifies, then breaking in production; forgetting the env var that feeds stateSecret when moving between environments.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/16512087998d6769. Report an issue: GitHub.