Last Updated: 2026-06-28 Status: Active Purpose: Durable record of known bugs, missing features, and improvement opportunities found during codebase analysis. Update this doc as items are fixed or new issues are found. Do not delete fixed items — mark them ✅ and add the date.
Scorecard
Audited 2026-06-28 against apps/blog-service source. Overall: 7.4 / 10
The core — API design, data model, multi-tenancy, security — is production-grade. Two entire feature surfaces (scheduled auto-publish, analytics) are non-functional and drag the overall score down significantly. Fixing B4 (cron trigger) and G2 (analytics pipeline) would push this to ~8.8.
| Parameter | Score | Notes |
|---|---|---|
| API Design | 9.0 | Clean admin/public/MCP split, consistent errors, cursor+page pagination |
| Data Model | 9.0 | 5 tables, 8+ indexes, soft-delete, SWR cache, partial unique indexes |
| Security & Auth | 9.5 | Gateway secret guard, 7 permission scopes, query-level tenant isolation |
| Multi-tenancy | 10.0 | Perfect — tenant_id on every table and every query, requireTenant middleware |
| Billing | 8.0 | creditGuard + grace window + idempotency is solid; MCP unpublish skips refund (B2) |
| Content Pipeline | 8.0 | HTML cache + renderer versioning + lowlight tokenization; carousel missing (B1) |
| Scheduling | 4.0 | Data stored correctly; no cron trigger exists — posts stay scheduled forever (B4) |
| Analytics | 2.5 | Binding declared, metric emits are no-ops, get-analytics returns zeros (B5, G2) |
| Newsletter Integration | 7.0 | REST publish fans out; MCP skips it (B3); no retry/DLQ (B7) |
| MCP Tools | 8.5 | 17 tools, full CRUD; behavioral divergence from REST undocumented (G4) |
| Testing | 9.0 | 14 test files, all major flows covered; minor creditGuard edge case gaps |
| Observability | 6.5 | Slow-request logger present; analytics not wired; METRICS absent from local dev |
| Feature Completeness | 7.5 | Full CRUD + schedule + soft-delete; no analytics, no full-text admin search (G6) |
| Slug Management | 5.5 | Collision retry is good; no redirect table — old URLs 404 permanently (B6, G3) |
Bugs (Broken Right Now)
These are things that are live and wrong. Fix before shipping new features.
B1 — Carousel node not rendered HIGH
Where: apps/blog-service/src/utils/tiptap-renderer.ts
tiptap-renderer.ts does not emit a <div data-type="carousel"> wrapper around carousel children. The @vlozi/blog SDK expects this wrapper to mount its carousel runtime — without it, posts with carousels render as a flat stack of <figure> elements. Any tenant who has inserted a carousel node has silently broken public posts.
Fix: Add carousel wrapper emission in tiptap-renderer.ts. Emit <div data-type="carousel"> around the rendered children when node type is carousel.
B2 — MCP unpublish-post silently skips 30-min credit refund HIGH
Where: apps/blog-service/src/routes/mcp/
The REST POST /admin/posts/:id/unpublish handler checks the 30-minute grace window and issues a credit refund if eligible. The MCP unpublish-post tool sets status=draft but never calls the grace-window refund path. Tenants unpublishing via MCP (agents, Cursor, Claude) permanently lose the credit they're entitled to. No warning is surfaced in the tool response.
Fix: Extract the grace-window refund logic into a shared function and call it from both the REST handler and the MCP tool. Or pipe MCP calls through the same handler.
B3 — MCP publish-post silently skips newsletter dispatch MEDIUM
Where: apps/blog-service/src/routes/mcp/
The REST POST /admin/posts/:id/publish fans out to newsletter-service via waitUntil(). The MCP publish-post tool charges the credit and flips status=published but never calls newsletter-service. Subscribers don't get notified. No warning is shown to the caller or in the tool schema description.
Fix (option A): Add waitUntil(notifyNewsletter(...)) to the MCP publish handler.
Fix (option B): Add an explicit skipNewsletter?: boolean param to the tool and document the default behavior clearly.
B4 — Schedule → auto-publish is permanently half-built MEDIUM
Where: apps/blog-service/src/routes/admin/posts.ts, wrangler.toml
POST /:id/schedule correctly sets scheduled_for and status=scheduled. However, there is no Cloudflare Cron Trigger or background worker to flip those posts to published at the target time. Scheduled posts stay status=scheduled forever unless manually published. The feature is broken-by-default — tenants can set a schedule time, see it in the UI, and nothing happens.
Fix:
- Add a Cloudflare Cron Trigger to
wrangler.toml(e.g.,crons = ["* * * * *"]for minute-level resolution or"0 * * * *"for hourly). - Add an
/internal/publish-scheduledhandler that queriesWHERE status='scheduled' AND scheduled_for <= NOW()and publishes each post (including newsletter fan-out and credit charge). - Secure the internal endpoint with
INTERNAL_KEYcheck (same pattern as other internal routes).
B5 — get-analytics MCP tool returns hardcoded zeros MEDIUM
Where: apps/blog-service/src/routes/mcp/
get-analytics returns { collected: false, views: 0, reads: 0 } for every post. The analytics ingestion pipeline does not exist. Any agent or AI workflow reading this data makes decisions on fabricated zeros.
Fix: See G2 (Analytics gap) below. Until the pipeline is built, the tool response should be explicit: { available: false, reason: "analytics_not_configured" } rather than numeric zeros.
B6 — Slug breaks permanently on title update LOW
Where: apps/blog-service/src/routes/admin/posts.ts, post.utils.ts
When a post title is updated, a new slug is generated. The old slug is abandoned with no redirect row written. Old URLs (bookmarks, search index results, backlinks) return 404 permanently.
Fix: See G3 (Slug redirect table) below.
B7 — Newsletter fan-out has no retry or DLQ LOW
Where: apps/blog-service/src/routes/admin/posts.ts publish handler
The waitUntil() call to newsletter-service is fire-and-forget. A transient error drops the newsletter silently. No failure is surfaced to the publisher and no retry is attempted. Subscribers don't know they missed a dispatch.
Fix: See P2 (Newsletter retry) in Improvement Scope below.
B8 — Credit not refunded on soft-delete of recently-published post LOW
Where: apps/blog-service/src/routes/admin/posts.ts delete handler
The 30-minute grace window is checked only in the unpublish handler. If a tenant hard-soft-deletes a post that was published within the last 30 minutes (without unpublishing first), the credit is not refunded. The tenant loses a credit they'd have recovered if they unpublished first.
Fix: In the delete handler, if status=published and published_at > NOW() - 30min, run the same grace-window refund before setting deleted_at.
Gaps (Missing, Not Just Broken)
Features that were never built. Tracked here for future sprints.
G1 — No cron auto-publish worker
The entire status=scheduled → status=published transition requires a background trigger that doesn't exist. Scheduling UI is live but non-functional.
What's needed: Cloudflare Cron Trigger + /internal/publish-scheduled handler. See B4.
G2 — Analytics pipeline not wired
METRICS Analytics Engine binding exists in wrangler.toml but is optional and absent from local dev. No ingestion from public reads. get-analytics returns zeros. There is no path from "a post was viewed" to "analytics data exists."
What's needed:
- Write an analytics event on every
GET /public/posts/:slugresponse (viawaitUntil()). - Build an aggregation query reading from the Analytics Engine dataset.
- Wire the result into
GET /admin/posts/:id/analyticsand theget-analyticsMCP tool. - Add
METRICSstub towrangler.tomldev config so local dev doesn't silently skip writes.
G3 — No slug redirect table
Slug changes (from title updates or restore collisions) abandon the old slug with no redirect. Old URLs return 404 permanently.
What's needed:
- Migration:
blog_post_slug_redirects (tenant_id, old_slug, new_slug, post_id, created_at) - When
PUT /admin/posts/:idchanges the slug, write an old→new redirect row. - When
POST /admin/posts/:id/restorerenames the slug, write a redirect row. - On
GET /public/posts/:slug404, check redirect table — if found, return301to the new slug.
G4 — MCP tool behavioral divergence is undocumented
publish-post and unpublish-post MCP tools have silent behavioral differences from their REST equivalents. This is undocumented in the tool schema description and will surprise any agent workflow.
What's needed: Update MCP tool descriptions to explicitly state:
publish-post: "Note: does not dispatch newsletter. Use the REST endpoint for full publish behavior."unpublish-post: "Note: does not trigger 30-minute grace-window credit refund."
G5 — blog_tags missing timestamps
blog_tags has no created_at or updated_at columns, unlike blog_categories. Can't sort, audit, or paginate by recency.
What's needed: Migration adding created_at TIMESTAMP NOT NULL DEFAULT NOW() to blog_tags.
G6 — No full-text search on posts
Admin list supports category/tag/status filters only. No ?q= search on title, excerpt, or content. Tenants with large post archives have no discovery mechanism beyond scrolling.
What's needed:
- Migration: add
search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(excerpt,''))) STORED. - GIN index on
search_vector. - Add
?q=param toGET /admin/posts— filter withWHERE search_vector @@ plainto_tsquery('english', ?).
G7 — No image optimization pipeline
featured_image_url is a raw URL stored as-is. No resizing, no CDN rewrite, no WebP conversion. Large originals are served directly to readers.
What's needed: Integration with media-service transform API (if available) or Cloudflare Images to rewrite stored URLs to optimized variants on publish.
G8 — Restore slug collision is silent
When POST /admin/posts/:id/restore detects a slug collision and auto-appends a suffix, the response doesn't call this out explicitly. The caller doesn't know the slug changed.
What's needed: Return { restored: true, slugChanged: true, oldSlug: "...", newSlug: "..." } when a collision is resolved.
Improvement Scope
Priority 1 — Users lose money or subscribers without these
| Item | Effort | Links |
|---|---|---|
| Wire cron auto-publish | Medium | B4, G1 |
| MCP unpublish: add grace-window refund | Small | B2 |
| MCP publish: add newsletter dispatch (or document skip) | Small | B3 |
| Credit refund on delete within grace window | Small | B8 |
| Fix carousel renderer | Medium | B1 |
Priority 2 — Correctness and trust
| Item | Effort | Links |
|---|---|---|
| Newsletter retry / DLQ | Medium | B7 |
| Update MCP tool descriptions for behavioral divergence | Tiny | G4 |
Fix get-analytics response to be explicit about unavailability |
Tiny | B5 |
Priority 3 — Missing features
| Item | Effort | Links |
|---|---|---|
| Slug redirect table | Medium | B6, G3 |
| Analytics ingestion pipeline | Large | G2 |
| Full-text search on admin list | Medium | G6 |
blog_tags timestamp migration |
Tiny | G5 |
| Restore slug collision explicit response | Tiny | G8 |
Priority 4 — Developer experience
| Item | Effort | Notes |
|---|---|---|
METRICS binding in local dev wrangler config |
Tiny | Currently absent from dev; writes silently skip |
| Integration tests: publish lifecycle (charge, idempotency, refund) | Medium | No evidence of coverage on creditGuard edge cases |
| Integration tests: soft-delete + restore + slug collision | Small | Restore path has slug collision logic that's untested |
| Image optimization pipeline | Large | G7 |
Fixed Items
Nothing fixed yet — this is the initial backlog from 2026-06-28 analysis.
How to Use This Doc
- Starting a sprint? Pick items from Priority 1 down.
- Fixed something? Mark it ✅ with the date and a one-line note on what changed.
- Found a new issue? Add it to Bugs or Gaps with severity, file location, and a proposed fix.
- Don't delete fixed items. They serve as a record of what was wrong and when it was resolved.