When a user connects a Google or Microsoft account, you receive a refresh token that can read their mail for months without them typing anything again. That token is a credential and deserves the handling you would give a password. Most token leaks are not exotic; they are ordinary database exposure meeting plaintext storage.
The blast radius is other people's mailboxes
A leaked users table is a bad week. A leaked tokens table is durable access to your customers' email and calendars, which makes it their incident too, and their customers' incident after that. The distance between an embarrassing disclosure and a headline is exactly what the stolen material can do, and a refresh token can do a lot: read mail, list contacts, watch a calendar, quietly, for as long as it stays valid.
That framing settles most design arguments early. You are not storing rows. You are storing keys to other companies' correspondence, and every decision below follows from that.
Disk encryption answers a different question
Encrypted disks and encrypted database volumes protect against a stolen drive. They do nothing about the exposure paths that actually occur: an injection bug, a leaked backup, a misconfigured replica, an internal tool with too much reach. In every one of those, the database happily returns rows, and the disk encryption was transparently undone before the data left the machine.
The answer is encrypting tokens at the application layer, before they reach the database. AES-256-GCM is the standard choice because it is authenticated: tampered ciphertext fails loudly instead of decrypting into something that almost works. Use a fresh nonce per token, store the nonce alongside the ciphertext, and the row in the database becomes useless without a key the database never sees.
Keys need a life cycle
The key cannot live next to the ciphertext, or you have built decoration, not encryption. Keep it in a secrets manager or key service, outside the database and outside the codebase. Version your ciphertext by storing a key identifier with each record, so two keys can coexist while a rotation runs. Rotation itself is a background job that decrypts with the old key and re-encrypts with the new one, and it should be boring enough to run on a schedule rather than only after a scare. If rotating keys requires downtime, you will not rotate them.
Scope the grant, then scope your own code
Least privilege applies twice. At the OAuth layer, request only the scopes the feature uses; a read-only feature has no business holding a send scope, and narrower grants also shrink what a provider's permission review has to cover. Inside your codebase, decryption belongs in one module with a short list of callers. Plaintext tokens must never appear in logs, error messages, or analytics events, and the code paths able to produce a plaintext token should be countable on one hand.
Disconnect means delete and revoke, both
When a user disconnects an account, two things must happen. Delete the ciphertext, and call the provider's revocation endpoint. Deleting alone leaves a live grant sitting at the provider under your app's name. Revoking alone leaves ciphertext in your database that you no longer have any reason to hold. Neither half is optional, and both belong in the same operation with retries on the revocation call.
Revocation also arrives from the other direction. A user can cut you off in their provider's security settings, and the first you hear of it is a refresh call that fails permanently. Treat that as a state change, not an error to retry: mark the connection broken, stop the sync jobs, and tell the user what happened and how to reconnect.
This is the floor
There is no advanced cryptography in any of this, just bookkeeping done carefully. Horato stores provider tokens encrypted with AES-256-GCM, hashes passwords with scrypt, and scopes every API call to an organization and application, and treats all of that as a floor rather than a selling point. If you are evaluating any vendor that touches mailboxes, ask them to describe their token handling at this level of detail. A vague answer is an answer.