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

# Operator-required cleanup

> Resolve every device, grant, and operation tied to a quarantined Perflo binding before clearing the connection.

# Operator-required cleanup

Use this workflow only when a connection is in `operator_action_required`. This state means the wrapper cannot prove both the local connection state and every remaining provider authority. The dashboard blocks connection changes until an operator resolves that ambiguity. Revoking a device does not revoke recipient grants created by that device.

An authenticated read rejection or failed credential refresh does not enter this state. It enters `reconnect_required`, where the customer can select **Reconnect** and sign in to the same Perflo account. A successful reconnect preserves the provider binding and restores service without an operator.

<Warning title="Safety-critical: follow exactly">
  This workflow protects authorities that a device lookup cannot resolve. Skipping a step, broadening the SQL predicate, or treating a timeout as confirmation can orphan a Perflo device, recipient grant, or payment result. Read every step before running it.
</Warning>

## When this applies

A connection enters `operator_action_required` when connection cleanup or local credential persistence becomes indeterminate and automated verification cannot recover it. The request may or may not have reached Perflo, or the local row may not describe provider state with confidence. The customer dashboard disables **Connect**, **Disconnect**, and **Resume** until an operator completes this workflow.

The wrapper retains the `device_id` and any cleanup ciphertext it can persist. A simultaneous Vault failure can leave no usable ciphertext, which is one of the cases the backup requirement below addresses.

Old-device cleanup after a same-account reconnect follows a different path. Before submitting revocation, the wrapper durably changes the cleanup to `operator_action_required` with `device_revocation_in_progress`; a process exit or local commit failure therefore cannot replay the provider write. Perflo success resolves the cleanup, while a definitive rejection that proves revocation was not accepted reopens it for a later credential attempt. A definitive credential rejection may be retried with an active credential belonging to the same customer and Perflo subject. A transport-uncertain revocation is never retried and remains `operator_action_required`. If a customer signs in to the wrong Perflo account during recovery, the wrapper rejects that identity and retains its encrypted access credential for cleanup. When Perflo omits that candidate’s device ID, selector-less revocation uses only that retained credential; it never falls back to the replacement credential. Those cleanup records do not block recovery unless their identity or revocation result is unprovable.

## The workflow

<Steps>
  1. **Record the incident identifiers.** From the database and logs, capture the
     `customer_id`, `provider_binding_id`, `device_id`, current `status`, `request_id`, and the
     incident or support ticket this cleanup belongs to. You will need all of them to write
     the guarded `DELETE` and to produce an audit record.

  2. **Inventory and resolve every authority.** Before treating device revocation as cleanup,
     list every pending device cleanup, mandate, and unfinished operation for the binding. A
     device cleanup must reach `resolved`, which means Perflo confirmed the exact-device or
     selector-less revocation request.
     For each mandate grant and one-off grant cleanup, obtain exact Perflo evidence that the grant
     is revoked, expired, or exhausted. For an indeterminate transfer, obtain its exact transaction
     outcome. An approval that completed without a grant ID requires Perflo to confirm that no
     authority remains for that customer and approval time window. A requested expiry, missing
     list row, timeout, or expired access token is **not** evidence.

       <Warning title="Only written provider confirmation counts">
         The entire workflow hinges on exact provider evidence. If you cannot get it, stop and
         open a Perflo support ticket. Keep the connection in `operator_action_required`; do not
         delete or replace its binding.
       </Warning>

  3. **Back up state together.** Back up the PostgreSQL, `vault-data`, and `vault-bootstrap`
     volumes together. See [Backups & recovery](operate/backups-recovery). These volumes are
     interdependent; a partial backup is not a valid restore point for this operation.

  4. **Stop the workloads that could race the repair.** Stop `api`, `worker`, and
     `scheduler` so no request can touch the connection while you edit it:

     ```bash theme={null}
     docker compose --env-file .env.server \
       -f compose.yaml -f compose.live.yaml -f compose.server.yaml \
       stop api worker scheduler
     ```

     On Kubernetes, scale the API, worker, and scheduler Deployments to zero.

  5. **Re-read the complete binding in a Postgres transaction.** Lock the exact connection,
     then verify that no live mandate authority or unresolved operation remains. Provider
     evidence must already have been applied to the matching mandate and operation states in
     a separately reviewed, incident-specific repair; preserve those rows and add an audit
     event containing only the incident ID and safe local IDs. Never put grant IDs,
     destinations, approval SIDs, or credentials in audit JSON.

     ```sql theme={null}
     BEGIN;
     SELECT id, customer_id, provider_binding_id, status, device_id
       FROM perflo_connections
      WHERE id = 'reviewed-connection-id'
      FOR UPDATE;
     SELECT id, state, upstream_grant_id, expires_at
       FROM mandates
      WHERE customer_id = 'reviewed-customer-id'
        AND provider_binding_id = 'reviewed-binding-id'
        AND (
          state = 'pending_approval'
          OR (
            upstream_grant_id IS NOT NULL
            AND state <> 'revoked'
            AND expires_at > now()
          )
         );

     SELECT id, device_id, reason_code, state, last_error_code
       FROM perflo_device_cleanups
      WHERE customer_id = 'reviewed-customer-id'
        AND provider_binding_id = 'reviewed-binding-id'
        AND state <> 'resolved';

     SELECT id, kind, state, failure_code, action_expires_at, next_reconcile_at
       FROM operations
      WHERE customer_id = 'reviewed-customer-id'
        AND provider_binding_id = 'reviewed-binding-id'
        AND (
          state IN (
            'requires_action', 'accepted', 'submitting', 'submitted', 'indeterminate'
          )
          OR (
            kind = 'transfer_grant_revoke'
            AND state <> 'succeeded'
            AND (action_expires_at IS NULL OR action_expires_at > now())
          )
        );
     ```

     All three cleanup and authority queries must return zero rows. If any returns a row, `ROLLBACK;` and
     resolve that exact item with Perflo before continuing.

  6. **Delete only the fully resolved binding.** Use the identifiers locked above and repeat
     both zero-authority checks inside the `DELETE` predicate:

     ```sql theme={null}
     DELETE FROM perflo_connections AS connection
      WHERE connection.id = 'reviewed-connection-id'
        AND connection.provider_binding_id = 'reviewed-binding-id'
        AND connection.status = 'operator_action_required'
        AND NOT EXISTS (
          SELECT 1 FROM perflo_device_cleanups AS cleanup
           WHERE cleanup.customer_id = connection.customer_id
             AND cleanup.provider_binding_id = connection.provider_binding_id
             AND cleanup.state <> 'resolved'
        )
        AND NOT EXISTS (
          SELECT 1 FROM mandates AS mandate
           WHERE mandate.customer_id = connection.customer_id
             AND mandate.provider_binding_id = connection.provider_binding_id
             AND (
               mandate.state = 'pending_approval'
               OR (
                 mandate.upstream_grant_id IS NOT NULL
                 AND mandate.state <> 'revoked'
                 AND mandate.expires_at > now()
               )
             )
        )
        AND NOT EXISTS (
          SELECT 1 FROM operations AS operation
           WHERE operation.customer_id = connection.customer_id
             AND operation.provider_binding_id = connection.provider_binding_id
             AND (
               operation.state IN (
                 'requires_action', 'accepted', 'submitting', 'submitted', 'indeterminate'
               )
               OR (
                 operation.kind = 'transfer_grant_revoke'
                 AND operation.state <> 'succeeded'
                 AND (
                   operation.action_expires_at IS NULL
                   OR operation.action_expires_at > now()
                 )
               )
             )
        );
     ```

       <Warning title="Never widen the predicate">
         Match the reviewed `provider_binding_id` even when device revocation has already cleared
         `device_id`. Never weaken either `NOT EXISTS` guard. A predicate without the binding and
         authority constraints can delete the only local evidence needed to contain a live grant.
       </Warning>

  7. **Commit only on exactly one deletion.** If the delete reports exactly `DELETE 1`, run
     `COMMIT;` and record the affected-row count in the incident. Otherwise run `ROLLBACK;`
     and **investigate instead of broadening the query**. Then restart the workloads and
     confirm the customer sees **Connect**:

     ```bash theme={null}
     make server-up
     ```
</Steps>

## Why this is not an API endpoint

The API does not expose this destructive binding deletion. The operator role can inspect and suspend activity but cannot assert grant revocation, settle an indeterminate payment, or clear credentials. Those actions require exact Perflo evidence that the device lookup cannot provide. The guarded SQL workflow remains the supported path for `operator_action_required`; ordinary credential recovery belongs to the customer reconnect flow.

## Related

* [Operator console](operate/operator-console): the read-and-suspend surface operators use to
  identify the connection and `request_id`.
* [Backups & recovery](operate/backups-recovery): the volume set to back up before step 3, and
  the rule against `docker compose down --volumes` on an active connection.
* [Vault](deploy/vault): why losing either Vault volume makes stored Perflo ciphertext
  unrecoverable.
