Thinking Through: Adding Persistent Logging to Contact Form Triage

Before adding a database table to a working feature, it's worth asking what actually changes — not just in the code, but in what you're now responsible for. A planning walkthrough for turning a stateless AI-triage feature into a persistent, queryable one, and the retention and privacy questions that come before the first line of SQL.

A pre-build planning note — the kind of thing you’d want settled before writing code, not after.

What Phase 1 already does

The email-tagging version (already built) is stateless. A message comes in, gets sent to Jev, gets a category and confidence back, and that gets stapled onto the email subject. Nothing is stored. If Jev is down, the form still works. If you never look at the tag, nothing accumulates. It’s the simplest version that could work.

What Phase 2 actually changes

Phase 2 isn’t really “add a database” — it’s a shift in what kind of feature this is. Phase 1 is a notification enhancement. Phase 2 is a record-keeping system. That’s a bigger commitment than it sounds like from the code diff alone, and it’s worth naming the tradeoff explicitly before building it:

What you gain:

  • Visibility over time — instead of reading each tagged email once and forgetting it, you get a real picture: how much of your contact form traffic is spam vs. real, whether collaboration requests are trending up, etc.
  • A second, browsable copy of messages in case an email gets lost or filtered

What you take on:

  • You’re now storing visitor-submitted message content indefinitely, in a system separate from whatever your form plugin already does (most forms already log submissions somewhere too — worth checking if you’re about to duplicate that)
  • A new thing that can go stale, get too big, or need cleanup
  • A new attack surface, however small — one more table, one more admin page, one more thing to keep patched

None of this means don’t build it — it just means Phase 2 deserves a retention policy decided before the first row gets written, not after the table has years of visitor messages in it.

Questions worth settling first

  1. Retention — Keep forever, or auto-purge after 30/90/365 days? A DELETE FROM ... WHERE created_at < NOW() - INTERVAL 90 DAY on a cron is a five-minute addition if decided up front, and an annoying migration if decided later.
  2. What counts as “message content” for privacy purposes — you’re storing a 200-character excerpt, not the full message. Is that enough to be useful, or does it need to be the full text? Shorter is safer; the tradeoff is losing context on longer messages.
  3. Who else can see the admin page — gated to manage_options (admin-only) by default. Worth confirming there’s no secondary WordPress user with lower privileges who shouldn’t see raw visitor messages.
  4. Where the table lives relative to backups — a new table is automatically included in a full DB backup, so visitor message excerpts become part of what’s backed up off-site too.

The build itself, broken into stages

  1. Create the table (via SQL Developer directly, using raw CREATE TABLE, or via WordPress’s dbDelta() — functionally equivalent, your call which is more comfortable)
  2. Add one line to the existing Phase 1 hook: log the result after a successful classification
  3. Add the admin page under Settings
  4. (if you decide retention matters) Add a scheduled cleanup job

Blog post angle

This maps cleanly onto a Concept Explainer / Lab Journal pairing:

  • Concept Explainer: “Stateless vs. stateful features — when does a WordPress snippet need its own database table?” using this exact before/after as the worked example
  • Lab Journal: the actual build — table creation, the one-line hook change, first real data, and whatever surprised you along the way

The “verifiable, dated, in-public” angle fits well here too — real (anonymized) stats from an actual contact form, once there’s 30 days of data, are a more concrete demonstration than most blog posts manage.