A database commit cannot send an email
A workspace owner invites Maya. Taskboard needs to store an invitation and send an email. Those actions happen in different systems. Postgres owns the invitation. An SMTP server accepts the message.
Your frontend may show one Invite button, but the server cannot make both systems succeed through an ordinary database transaction.
Two possible orders
If Taskboard sends the email first, then its database insert fails, Maya receives a link to an invitation that does not exist. If Taskboard stores the invitation first, then its process crashes, the invitation exists but Maya receives nothing.
Here is a teaching version of the second order:
await db.insert(invitations).values(invitation);
await mailer.sendMail(message);
await orders the operations inside this process. It does not protect the gap between them. After the insert, the process can stop because of a deploy, a machine failure, or a forced shutdown. A try block cannot catch the disappearance of its own process.
Wrapping both lines in a database transaction also creates a problem. An SMTP server does not participate in that transaction. Postgres can roll back the invitation after the message has already left.
Store the obligation
Taskboard stores the invitation and an email job in one Postgres transaction. Both records commit, or neither commits. The HTTP response means the server accepted the invitation and saved the delivery work. A separate worker later attempts delivery.
The observable result changes. The owner can see an invitation immediately, while email delivery may happen later. A temporary SMTP failure does not require the owner to recreate the invitation.
This pattern makes unfinished work survive a process restart. It does not make delivery instant or guarantee that an inbox accepts the message. The next lessons follow the saved job through those failures.
Read the invitation transaction in Read api/src/tenancy.ts and the worker in Read api/src/jobs.ts.
A database transaction protects database changes. Store the obligation to perform an external action before relying on a process to perform it.
Why is a fire-and-forget promise insufficient?
The promise exists in process memory. If the process stops, Postgres has no record that email still needs delivery. A durable job records that obligation outside the process.