we-promise/sure · warning · ActiveRecord::RecordNotDestroyed

Cannot destroy demo monitoring API key

Error message

Cannot destroy demo monitoring API key

What it means

ApiKey#delete overrides the model's delete to raise ActiveRecord::RecordNotDestroyed("Cannot destroy demo monitoring API key") when the record is the seeded demo monitoring key (display_key == DEMO_MONITORING_KEY). destroy goes through delete, so both normal and skip-callbacks destruction paths are blocked. Like revoke!, this protects the demo family's external monitoring credential from being removed piecemeal — the demo teardown (Demo::DataCleaner) is the only sanctioned remover, and it uses delete_all to bypass exactly this guard deliberately.

Source

Thrown at app/models/api_key.rb:72

  def revoked?
    revoked_at.present?
  end

  def expired?
    expires_at.present? && expires_at < Time.current
  end

  def key_matches?(plain_key)
    display_key == plain_key
  end

  def revoke!
    raise ActiveRecord::RecordNotDestroyed, "Cannot revoke demo monitoring API key" if demo_monitoring_key?
    update!(revoked_at: Time.current)
  end

  def delete
    raise ActiveRecord::RecordNotDestroyed, "Cannot destroy demo monitoring API key" if demo_monitoring_key?
    super
  end

  def demo_monitoring_key?
    display_key == DEMO_MONITORING_KEY
  end

  def update_last_used!
    update_column(:last_used_at, Time.current)
  end

  # Get the plain text API key for display (automatically decrypted by Rails)
  def plain_key
    display_key
  end

  # Temporarily store the plain key for creation flow
  attr_accessor :key

View on GitHub (pinned to e69894adb9)

Solutions

  1. Use the sanctioned teardown: Demo::DataCleaner removes demo records and delete_all's the key, bypassing the guard on purpose
  2. Exclude the key from bulk deletion: ApiKey.visible.each(&:destroy) or next if k.demo_monitoring_key?
  3. When deleting the demo admin user, run demo cleanup first so the cascade never reaches the protected key
  4. In tests, never use DEMO_MONITORING_KEY as a factory value; generate random keys

Example fix

# before
user.api_keys.each(&:destroy)
# => ActiveRecord::RecordNotDestroyed: Cannot destroy demo monitoring API key

# after
user.api_keys.visible.each(&:destroy)        # guard never trips
# full demo teardown instead of piecemeal destroy:
# Demo::DataCleaner.new.run  (uses delete_all for the reserved key)
Defensive patterns

Strategy: validation

Validate before calling

# In cleanup code
ApiKey.visible.each(&:destroy)                       # skips demo key
# or guard explicitly
keys.each { |k| k.destroy unless k.demo_monitoring_key? }

Type guard

def destructible_api_key?(key)
  !key.demo_monitoring_key?
end

Try / catch

rescue ActiveRecord::RecordNotDestroyed => e
  if e.message.include?("Cannot destroy demo monitoring API key")
    next # reserved key: only Demo::DataCleaner may remove it
  else
    raise
  end
end

Prevention

When it happens

Trigger: Running api_key.destroy or api_key.delete on the demo monitoring key from console/script; a generic 'purge old API keys' cleanup task iterating all records; dependent => :destroy on a user being deleted who holds the demo key (Demo::Generator attaches it to the demo admin user) — destroying that user cascades into this raise; factories/tests that accidentally instantiate a key with the demo display_key value.

Common situations: Deleting old/unused keys during an audit and catching the demo one; tearing down a demo admin user without running the demo cleaner; test suites with loose factories producing the reserved display_key; copying production data to staging and cleaning it with naive destroy loops.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/52bd5091b3598199. Report an issue: GitHub.