ZapBounce and PostgreSQL
PostgreSQL is where a lot of application contact data lives, and verification here is a schema question before it is an API one.
The schema decision that matters: verdicts belong in their own table with a timestamp, not as a column on the users table.
A status column on the users table has no expiry
Adding email_status to the users table looks simplest and creates a value with no age. Six months later it is quoted in a query as current, and it describes a check nobody remembers running.
A separate table with a checked_at timestamp makes the age explicit, and a view can apply the staleness rule so every consumer inherits it rather than remembering it.
The second detail is the index. A partial index on rows needing a check keeps the candidate query fast as the table grows, which a full index on a low-cardinality status column does not.
How the data moves
A verdicts table keyed on the address
Address, result, reason, smtp_code, checked_at. One row per address, not per user.
A partial index for the candidate query
Indexing only rows where checked_at is old or null keeps the scan small however large the table gets.
Batch through a scheduled job
A worker selects candidates, submits a batch, and upserts results on the webhook.
Expose a view with the staleness rule built in
So a consumer querying deliverable addresses cannot accidentally read a two-year-old verdict.
Setting it up
- Create an email_verdicts table keyed on the address with a checked_at timestamp.
- Add a partial index covering rows where checked_at is null or older than your threshold.
- Write a worker that selects candidates, submits them as a batch and stores the batch id.
- Handle the completion webhook by upserting results into the verdicts table.
- Create a view joining users to verdicts with the staleness condition applied.
- Point application queries at the view rather than the table.
PostgreSQL: common questions
Column on users or a separate table?
Separate table. A column on users has no natural place for the check timestamp, and a verdict without one silently expires.
Why a partial index?
The candidate query only ever wants stale or unchecked rows. A partial index keeps that scan small as the table grows.
Where should the staleness rule live?
In a view, so every consumer inherits it. A convention in documentation is a rule nobody runs inside.
Check a PostgreSQL export today
100 free checks a month, no card. Unknown results and duplicates are never billed.