Verification email bugs are annoying because the failure usually lands between systems. The REST API says the request passed, the worker says it sent something, and QA says the inbox never showed the message. If those three views do not share one identifier, your incident review turns into log archaeology pretty fast.
What has worked better for me is adding a correlation id that travels from the auth request, into PostgreSQL, through the worker, and finally into any inbox assertion data. It is a small backend habit, but it makes Authentication issues much easier to explain and fix.
Why auth systems lose the email trail
Many auth services already store useful bits:
- user id
- email template
- provider response id
- retry count
That still is not enough when support asks a simple question: "Did this exact signup request produce the message that QA checked?"
The gap happens because delivery metadata and inbox metadata are often recorded in different places with different keys. A worker might log job_id, while the test harness stores only the recipient address. If the address was reused, or a resend happened, your evidence gets fuzzy. I have seen this go wrong even in otherwise clean systems, and the debugging cost was higher than the schema fix.
The correlation id shape I keep in PostgreSQL
I like giving every email-producing auth action one correlation_id at request time. That id gets written to the outbox row and also to a receipt table.
create table auth_email_receipts (
id bigserial primary key,
correlation_id uuid not null,
user_id bigint not null,
email_job_id bigint not null references email_jobs(id),
template text not null,
delivery_status text not null,
provider_message_id text,
inbox_observed_at timestamptz,
inbox_observation_source text,
created_at timestamptz not null default now()
);
The important part is not fancy SQL. It is that correlation_id survives every handoff. PostgreSQL is a good fit here because inserts are cheap, ordering is obvious, and querying one incident path stays boring. Boring is good, honestly.
I also keep inbox_observation_source optional. Sometimes the inbox check comes from Playwright, sometimes from a manual run, and sometimes not at all. Your source of truth should still be the backend evidence trail, not whether an inbox tool happened to respond that minute.
How the API and worker pass the same evidence forward
My preferred flow is:
- The signup or password-reset endpoint creates
correlation_idbefore the transaction commits. - The API writes the outbox row with that id.
- The worker copies the same id into the delivery receipt when it claims and sends the job.
- Test or QA tooling records inbox observations against the same id.
That means one query can answer the whole story:
select correlation_id, template, delivery_status, provider_message_id, inbox_observed_at
from auth_email_receipts
where correlation_id = $1
order by created_at asc;
When teams add abortable inbox polling on the client or in QA tooling, this shared id keeps polling results attached to the right auth event instead of just "whatever arrived for this mailbox". That matters more than people expect.
If I need an external mailbox for a staging check, I keep it narrow and contextual. A service like email temporary free can help validate that the user-visible message arrived, but I still treat it as supporting evidence. The receipt and outbox data should explain the backend path first.
Where inbox tools fit without owning the truth
Inbox tools are useful, but I try not to let them define correctness by themselves. Mailboxes are noisier than your database. Polling delays happen. Test addresses get reused. Someone pastes the wrong temp org mail into a manual checklist and now the investigation starts from a bad premise.
That is why I separate two questions:
- Did the auth backend create and send the intended message?
- Did an inbox observer later confirm the message was visible?
Those are related, but not identical. The first one should be answerable entirely from your application records. The second one adds user-facing confidence. For teams working on privacy-sensitive flows, invite email privacy checks are a good reminder that inbox validation should stay scoped and deliberate, not sprayed across logs.
One more small thing: make the correlation id visible in operator tooling. If an on-call engineer can paste one id into a dashboard and see the request, outbox row, worker attempt, and inbox observation together, the mean time to understand drops a lot. That sounds obvious, but many systems almost do this and then miss by one join key. I did that once, and it was a bit embarrasing.
Q&A
Should the user id be enough?
Usually no. A single user can trigger several auth emails in a short window, so you need event-level identity, not just actor-level identity.
Do I need a new table for this?
Not always, but a dedicated receipt table keeps the timeline cleaner than packing every state change onto the user record.
What if the provider already gives me a message id?
Keep it, but do not depend on it as your primary join key. You need an id created inside your system before the provider call happens.
Top comments (0)