SaaSClerk Blog · How the offboarding checklist is built
Offboarding checklist: cancel the SaaS tools an employee leaves behind
Short answer: a departed employee does not cancel their own seats, and the person who pays the card often never logs into the tool. Write the tools down as a per-person list, close each one with a timestamp, and show recoverable spend only for prices you actually know.
The leak is structural, not careless. The seat is billed to a company card; the login belongs to a person who no longer works there; the renewal email goes to a mailbox someone disabled on the last day. Nobody receives a signal that says "this charge is now for nobody". The charge simply continues, quietly, until an audit happens to look in the right place.
Why a spreadsheet stops working
A spreadsheet of departed employees can record the intent to cancel. It cannot record that a specific tool was actually closed, by whom, and when. Once two people edit the same row, the answer to "did we cancel Notion for Alex?" becomes a matter of memory. Memory is the thing that left with Alex.
Store the list, not the intention
The minimum honest data model is two tables: one row per departing employee, one row per tool under that person. The parent row holds the person and an overall status; the child row holds the provider, an optional cancel link, the item status, and the timestamp when it was closed.
model OffboardingList {
id String @id @default(uuid())
organizationId String
employeeName String
employeeEmail String
status OffboardingStatus @default(PENDING)
notes String?
items OffboardingItem[]
}
model OffboardingItem {
id String @id @default(uuid())
offboardingListId String
subscriptionId String?
providerName String
cancelUrl String?
status OffboardingItemStatus @default(PENDING)
completedAt DateTime?
}Three details carry the whole design. The item status is an explicit enum (open, closed, intentionally kept) rather than a free-text note. completedAt is written when the item closes, which turns the list into an audit trail instead of a to-do list. And subscriptionId is optional: a checklist that only accepts tools already in your inventory cannot cover the tool nobody tracked, which is exactly the kind of tool that keeps charging.
Close items, and recompute the parent
The parent status should never be typed in by hand. Derive it from the children each time one changes: any open item means in progress, no open items means complete, and nothing touched means pending. That derivation is cheap and keeps the two levels from disagreeing.
const open = items.filter((item) => item.status === 'PENDING').length
const listStatus =
open === 0 ? 'COMPLETED' : open === items.length ? 'PENDING' : 'IN_PROGRESS'Show recoverable money only when the price is known
A checklist can display a monthly figure, but only by summing prices that exist in the inventory. For each open item, look up its linked subscription and add its stored monthly cost. If an item has no price on file, it contributes nothing to the number and is not silently counted as zero.
This distinction matters on the screen. "$12.50 a month still recoverable" is a claim backed by two rows. A checklist that assigns an estimated price to an unknown tool teaches the reader to distrust every number on the page, including the correct ones.
Server rules that keep the list trustworthy
A checklist about people and money has to be scoped tightly. Four rules are enough for a first version:
- The organization comes from the session, never from the request body.
- A linked subscription must belong to that organization; a stray id is dropped, not trusted.
- Marking an item closed requires the item to belong to a list in the same organization; anything else is a 404, not a 403 that reveals the row exists.
- Closing an item writes a timestamp once; reopening clears it, so the record never claims two truths.
A note is not a field
Every tool has a detail that fits nowhere: "seat count 3", "cancel via the workspace owner", "keep for the contractor". Without a dedicated column, those details belong in the parent note, appended one line per item, rather than dropped at the API boundary. Losing an operator's note is how a checklist loses its audience.
What this looks like in practice
The person responsible opens the checklist on the day of departure, picks the tools already in the subscription inventory, and adds the rest by hand with a cancel link. Over the following days each item is closed with one button, and the progress bar is the only status anyone needs to read. The list stays after the person is gone, which is the point: the record outlives the memory.
Inside SaaSClerk this is the offboarding checklist, built on two tables that were already in the database. No new schema, no new integration, no manual reconciliation at the end of the month.
Related reading
Price the leak you already have
Before building a process, measure the waste: the unused-seat calculator takes seat counts and prices and returns the monthly and yearly figure, with no account required.