I needed a lead engine for a chocolate brand’s hotel and corporate gifting outreach — not another SaaS trial and not a spreadsheet that dies after week two. So I shipped an admin-only WordPress CRM plugin with Cursor: import research CSVs, park draft emails, and only send after an administrator types a confirmation. Nothing auto-mails.
This is a build log, not a product pitch. The CRM lives on the brand’s WordPress site. The pattern is what I want on Builders: a real tool, pair-programmed with an AI coding agent, with the sharp edges left in.
What I actually shipped
- Custom plugin, Administrators only (
manage_options) — no public routes, no anonymous REST - Pipeline board + list across ten stages (Research → Ready to contact → Draft ready → Sent → … → Won / Lost / Nurture)
- CSV import for research batches with header aliasing and SHA-256 dedupe on org + property + email/contact
- Draft compose that parks Wave 1 hotel emails in Draft ready
- Send path that requires typing
SEND, then useswp_mail/ SMTP — import never sends - Activity log for draft and sent events
Offline dry-run on the research folder: hundreds of CSV rows collapsed to ~365 unique leads after dedupe. Wave 1 hotel drafts attach to matching leads when those rows exist.
How Cursor helped (and where I stayed in the loop)
Cursor was the pair-programmer for plugin scaffolding, admin UI, importer edge cases, and the “never send on import” guardrails. I kept the product rules:
- Admin-only. If you are not a WordPress administrator, you do not see the CRM.
- No invented contacts. Only CSV / draft markdown content is stored.
- Human confirmation before any outbound. Typing
SENDis deliberate friction. - SMTP identity is separate from “looks good in the editor.” From / Reply-To and the visible signature have to match the mailbox the brand actually owns.
That last one bit us. Mail hosting, MX/SPF, and “Send mail as” were not optional polish — without them the CRM is a fancy draft folder. We wired SMTP with an app password and kept testing until a From address the brand wants actually left the server.
What broke
- Mailbox before CRM: Missing MX/SPF meant brand-domain mail never landed. The plugin was ready; the domain was not.
- Auth vs visible From: SMTP authenticated as one mailbox while the visible From needed another. Getting that pair right took more time than the pipeline UI.
- CSV messiness: Headers differed across research batches. The importer needed aliases or every import looked “empty.”
- Temptation to auto-send: Easy to wire “import → send.” We refused. Wave 1 stays parked until an admin confirms.
What I left out on purpose
- No public lead form tied into this plugin (the site form can still capture separately)
- No automatic brochure PDF send
- No “AI writes the cold email and blasts hundreds of hotels” loop
- No Laya / local-agent runtime story in this post — this CRM is WordPress + Cursor, not a homelab agent
If you want the Cursor-on-mustafa.net side of how I ship interactive tools, I wrote up the free local LLM hardware checker separately. For local model hardware context, the Local LLMs hub is still the front door.
Ship checklist (if you copy the pattern)
- Lock capability checks and nonces on every admin AJAX action.
- Import and send are different code paths; import must not call
wp_mail. - Prove SMTP with a real From the customer will reply to before you call the CRM “live.”
- Keep research CSVs boring and deduped — the CRM is only as honest as the sheet.
FAQ
Is this a public mustafa.net app?
No. It is a WordPress plugin on the brand site. This post is the build story.
Does import send email?
No. Import only creates or updates leads and can park drafts. Send requires an administrator confirmation.
Why Cursor instead of a hosted CRM?
I needed admin-gated stages, research CSV import, and send friction on the same WordPress install the site already uses — without another monthly seat tax for a tiny outbound motion.
