R RockAI docs

Architecture

APP_KEY rotation

Pitchbar encrypts every customer-controlled secret at rest under the Laravel APP_KEY. If that key leaks, you need to rotate to a new one without losing access to existing data. The security:rotate-app-key Artisan command makes the rotation safe and idempotent.

Columns covered

  • workspaces.cta_context_secret
  • workspaces.byok_keys
  • workspaces.slack_webhook_url
  • workspaces.teams_webhook_url
  • webhook_subscriptions.secret
  • workspace_api_tokens.shopper_signing_secret
  • dsr_requests.result_payload
  • integration_connections.credentials_encrypted
  • sources.credentials_encrypted
  • app_settings.* (Stripe, PayPal, Razorpay, Cloudflare, OpenAI, OpenRouter, mail, internal queue secrets)

Rotation flow

  1. Generate a new key:
    php artisan key:generate --show
    # copy the output
  2. Add the new key as the active APP_KEY and move the old key into APP_PREVIOUS_KEYS (comma separated if multiple). Example:
    APP_KEY=base64:NEW_KEY
    APP_PREVIOUS_KEYS=base64:OLD_KEY
  3. Deploy. Laravel will decrypt rows with either key (new takes priority on writes); reads using the old ciphertext fall back to the previous key. The app keeps serving uninterrupted.
  4. Run the sweep:
    php artisan security:rotate-app-key --confirm-production
  5. The command iterates every encrypted column in chunks of 200 rows, decrypts with the current key (or the previous-key fallback), and re-encrypts with the new key. Skipped rows (rows whose ciphertext cannot be decrypted) are reported but do not abort the sweep.
  6. Once the command completes, remove APP_PREVIOUS_KEYS from your env. The old key is no longer needed for any row.

Safety flags

  • --confirm-production โ€” required in production environment. Omit in staging / local.
  • --dry-run โ€” counts rows + columns that would be re-encrypted without writing. Useful to preview the surface before committing.

Idempotency

Running the command twice is harmless. The second pass decrypts and re-encrypts the same values; the ciphertext bytes change (fresh IV) but the plaintext remains intact.

What if a row cannot decrypt?

The command logs the column and row id to the console and moves on. That row stays on whatever key produced its current ciphertext. If you removed APP_PREVIOUS_KEYS before the sweep completed, those rows are unrecoverable โ€” restore from a backup and re-run with the previous key in env.

Audit footprint

The command does not write to audit_logs on its own โ€” but the operator who runs it should record the rotation in their change-management system. If you want every rotation auto-audited, wrap the artisan call in a deploy script that emits an audit row.