---
title: Build a Safe SaaS Tenant Offboarding and Data-Retention Workflow on AWS
description: Delete is a late state. A legal hold stops the machine in HELD. One Scheduler invocation on the retention date sits inside the 14 million free EventBridge Scheduler invocations a month. The first write revokes access. It does not delete objects.
url: https://www.factualminds.com/blog/saas-tenant-offboarding-data-retention-aws/
datePublished: 2026-10-19T00:00:00.000Z
dateModified: 2026-10-19T00:00:00.000Z
author: palaniappan-p
category: Cloud Architecture
tags: saas, data-retention, amazon-cognito, aws-step-functions, amazon-s3
---

# Build a Safe SaaS Tenant Offboarding and Data-Retention Workflow on AWS

> Delete is a late state. A legal hold stops the machine in HELD. One Scheduler invocation on the retention date sits inside the 14 million free EventBridge Scheduler invocations a month. The first write revokes access. It does not delete objects.

On **19 October 2026**, the dangerous offboarding is the one that deletes first and looks for the export afterwards. The [GDPR guide](https://www.factualminds.com/blog/gdpr-compliance-aws-saas-data-protection/) explains data-subject rights and why backups outlive a delete. This page is the tenant, not one person inside a tenant: the customer is leaving, and the prefix has to follow a state machine.

SES "tenants" are a different product. They stay on the SES tenant guide. Isolation, if you have not picked it, stays on the [silo, pool, and bridge post](https://www.factualminds.com/blog/saas-multi-tenancy-on-aws-silo-vs-pool-vs-bridge-model/).

> **Reproduce this** — Copy the [offboard states](https://www.factualminds.com/examples/architecture-blog-2026/tenant-offboarding/offboard-states.md). If delete is the first row, the sheet is wrong.

## Who this is for

The operator who closes accounts, the compliance partner who asks about legal hold, and the engineer who can disable a user without dropping a bucket.

## What you get, and what you do not

You get revoke, a checksummed export, a hold branch, a scheduled delete, and a second approval. You do not get a guarantee that every backup copy is gone. You do not get an undelete.

**Opinionated take:** two approvals, and hold beats the calendar. The trade-off is a tenant who still occupies storage until the retention date. That storage is cheaper than a deletion you cannot explain.

## Architecture

An authorised event starts a Step Functions Standard workflow. The tenant item from the [provisioning workflow](https://www.factualminds.com/blog/automate-saas-tenant-provisioning-aws/) is the source of status. Cognito `AdminDisableUser` runs before any export is treated as final, so the tenant cannot write during the copy. A legal-hold flag stops the machine in `HELD`. Otherwise EventBridge Scheduler gets a one-time schedule at the retention date. The delete function runs only from `DELETE_APPROVED`.

```mermaid
flowchart LR
  request[OffboardRequested] --> revoke[RevokeAccess]
  revoke --> export[VerifiedExport]
  export --> hold{LegalHold}
  hold --> held[Held]
  hold --> clock[RetentionSchedule]
  clock --> approve[SecondApproval]
  approve --> delete[DeletePrefix]
```

## Walkthrough

**1. Require an approver id** on the event. No id, no state change.

**2. Revoke.** Context: Amazon Cognito `AdminDisableUser` for the tenant's user pool. Then revoke refresh tokens if your pool supports the revoke call you have enabled. Write `REVOKED` only after the disable call succeeds. A failed disable stops the workflow. Do not export a tenant who can still sign in and change data under you.

**3. Export and checksum.** Copy the tenant prefix to an export bucket the application cannot write. Store the checksum on the item. `EXPORTED` requires the checksum. The [S3 security post](https://www.factualminds.com/blog/aws-s3-security-best-practices-preventing-data-exposure/) is where Object Lock and block-public-access belong. Turn them on for the export bucket before you trust it as evidence.

**4. Hold or schedule.** If `legalHold` is true, set `HELD` and stop. Clearing a hold is a new authorised event, not an edit in the console that the workflow does not see. If there is no hold, create a Scheduler schedule at the retention timestamp with an input that contains the tenant id and nothing else.

**5. Second approval, then delete.** On the retention date the schedule starts a function that only notifies. A person sends the task token. The delete function reads the item and returns without deleting when status is not `DELETE_APPROVED` or when `legalHold` is true. Delete is `DeleteObject` on keys listed under that tenant prefix, not a bucket delete.

Cancellation before `DELETE_APPROVED` is a new event that sets `ACTIVE` and deletes the schedule. It does not recreate objects.

## IAM

`cognito-idp:AdminDisableUser` on the one user pool. `s3:GetObject` and `s3:PutObject` on the export prefix. `s3:DeleteObject` only on the tenant prefix, and only on the delete function's role. `scheduler:CreateSchedule` and `scheduler:DeleteSchedule` on the schedule name prefix `offboard-`. The delete role must not have `s3:DeleteBucket`.

## Failure and retries

A failed export leaves the tenant `REVOKED`. That is safer than `ACTIVE`. The customer cannot write, and you still have the source objects. Retry the copy. Do not skip to delete to "clean up" a failed export.

The delete function is idempotent: a missing key is success. A second run after `DELETED` returns success and deletes nothing else.

## Cost, as an assumption

One one-time Scheduler invocation per closed tenant. The [published Scheduler example](https://aws.amazon.com/eventbridge/pricing/) includes **14 million** invocations a month free, then **$1.00 per million**. The invocation is not the cost. The export copy and the days you keep it are S3 storage and requests. Price those on the S3 pricing page for the Region and storage class you pick. Not a measured saving.

## Testing

Run a tenant fixture with no hold. Expect disable, an export object, a schedule, and no deleted source key before the token. Set `legalHold` and expect `HELD` with the source still present. Call the delete function directly with status `EXPORTED`. Expect no deletion.

> **What broke** — Lab, 19 October 2026. Not a customer account. The schedule's target role could delete any key in the bucket, and a bad tenant id in the input matched an empty prefix that the code treated as "delete all". Detection: the test aborted because the function's guard saw a prefix shorter than the tenant id pattern. Recovery: refuse a prefix that does not contain a `ten_` id, and scope the role to `tenants/*`. Lesson: an empty prefix is a bucket wipe.

## Production checklist

- Approver required at request and again before delete.
- Legal hold cannot be cleared by the delete function.
- Export checksum is present before the schedule exists.
- Delete role cannot delete the bucket or an unscoped prefix.
- Token revoke is explicit, not assumed from disable.
- Backup retention is documented in the GDPR guide, not ignored.

## What This Post Doesn't Cover

Data-subject access requests for a single user. SES tenant teardown. How long a regulation tells you to keep a record. That number comes from counsel, then you type it on the sheet.

## What to Do This Week

1. Pick one non-production tenant and walk the sheet until `EXPORTED`.
2. Prove the delete function no-ops when the status is wrong.
3. Only then attach a schedule.

Closing a tenant cleanly is part of [managed services](https://www.factualminds.com/services/aws-managed-services/) when the account is already in production and the gap is the state machine.

## FAQ

### When should we not run this offboarding workflow?
Skip it when you only need to answer one person's access or erasure request inside a tenant that stays a customer. That is the GDPR guide. Also skip it if you cannot name an approver. An unattended delete is not a workflow.

### What could go wrong if a lifecycle rule deletes the prefix on the same day?
The export is incomplete, or a legal hold is ignored, because lifecycle does not read your tenant item. Lifecycle is a backstop for objects you have already decided to expire. It is not the approver. The S3 security article covers Object Lock. This state machine is what decides the tenant may reach that point.

### Can we undelete after DELETE_APPROVED?
Do not plan on it. Cancellation is allowed before that state, by a new authorised event, and only if the objects are still there. After the delete function has removed the prefix, this design does not restore it. Backups are a separate retention question in the GDPR guide.

### Does disabling a Cognito user end every session?
AdminDisableUser prevents new sign-in. Existing tokens live until they expire unless you also revoke them with the Cognito revoke API your user pool is configured for. Call both, and record which call succeeded. Do not claim sessions died because the user looks disabled.

---

*Source: https://www.factualminds.com/blog/saas-tenant-offboarding-data-retention-aws/*
