The outbox records unfinished work
Maya's invitation and its email job belong to the same decision. If an invitation exists, Taskboard must have a durable record of the email work. A transactional outbox makes that relationship explicit.
An outbox is a database table containing work that another process will deliver. Taskboard's jobs table performs this role. The table lives in the same Postgres database as the invitation, so one transaction can update both.
What commits together
This teaching version omits the token and permission checks to expose the transaction boundary:
await db.transaction(async (tx) => {
await tx.insert(invitations).values(invitation);
await tx.insert(jobs).values(emailJob);
});
Passing tx to both inserts matters. An insert through the ordinary database connection would happen outside this transaction. The callback alone does not include every operation called nearby.
Suppose the process stops after the invitation insert but before the job insert. Postgres rolls back the unfinished transaction. Maya has no invitation and no email job. If the process stops after commit, both records survive, and the worker can find the job after restart.
You can now explain the response. Success means that Taskboard saved the invitation and delivery obligation. Success does not mean the recipient opened an email.
Keep the payload deliberate
A job needs enough data to perform its action. It also needs a defined type so the worker chooses the correct handler. Email jobs may contain a recipient, subject, and body. Those values can include invitation links, so access to the jobs table is security-sensitive. Logging an entire payload can expose a token.
An outbox solves the database-to-worker gap. It does not atomically combine Postgres with SMTP or Stripe. A worker can deliver a message and crash before marking the job complete. That separate gap explains why the course talks about at-least-once attempts rather than exactly-once external delivery.
Follow job records in Read api/src/schema.ts and their creation and execution in Read api/src/jobs.ts.
Commit the domain change and its job together. Treat the saved job as work still owed, not proof that an external service completed it.
Does putting the job in Redis after commit have the same guarantee?
No. Postgres could commit before the process reaches Redis. An outbox closes that gap because both records share one database transaction. Redis can still be part of a larger design, but it needs a reliable transfer mechanism.