Skip to content

Publishing App Content

How produced app content reaches Directus and the app. Rails only — the research/publish copilot skills (Phase 1, naluma-ai-marketplace) automate these steps. Terms: glossary.md. Lifecycle: content-lifecycle.md. Editorial standards: authoring/.

Path

repo-first item files (sessions/, coach-conversations/, sounds/) + programme-map.yaml
  → validate the body against the Directus schema for its type (reference-manifest)
  → publish via Directus MCP (POST /items/<collection> + _translations + asset refs)
  → content.* (Supabase)
  → app reads v_published_*
Research source: content-automation Neon (read-only).

Read — content-automation Neon (research source)

  • DB: the naluma-content-automation Neon database. Connection via its DATABASE_URL (see naluma-content-automation/.env.example + the naluma-root secrets inventory). A Neon MCP may also be used. Read-only — never write here.
  • Tables to mine (verified against naluma-content-automation/src/db/tables.py):
  • articles.research_dossier (JSONB) — grounded evidence packages
  • articles where content_type='research_news' — measured research summaries (≈ insights)
  • source_items — triaged, scored research items
  • trusted_sources — authoritative-source allowlist
  • vault_notes / vault_chunks — pgvector-embedded research vault (hybrid search)

Write — Directus MCP (publish target)

  • Endpoint: dev https://cmsdev.naluma.space/mcp, prod https://cms.naluma.space/mcp (dev hostname confirmed in naluma-directus/CLAUDE.md and README.md; prod not yet live). Requires MCP_ENABLED=true (set in fly.dev.toml [env]) + the in-admin toggle at Settings → AI → Model Context Protocol → Enable MCP Server (owned by naluma-directus).
  • Role: Content Editor (MCP) (machine identity info+claude@naluma.app, confirmed in naluma-directus/config/users.yaml). CRU on content.* except legal_pages / legal_pages_translations (exclusion confirmed in naluma-directus/config/roles.yaml).
  • Token: personal MCP bearer token — see naluma-root docs/secrets-inventory.md (~/.naluma-env/directus-mcp.env / DIRECTUS_TOKEN). Never paste the token in this repo. Current storage: ~/.zshrc (see the Known ambiguities section of the secrets inventory for the migration TODO).
  • Connect to Claude Code: add the Directus /mcp server with the bearer token (claude mcp add or session MCP config using DIRECTUS_URL + DIRECTUS_TOKEN from directus-mcp.env).
  • Publish procedure:
  • POST /items/sessions { slug, locale, status, template_type, category, program_week, tier, content, image_asset_id, audio_asset_id }
  • translations: POST /items/sessions_translations { sessions_id, languages_code, title, … }
  • coach_conversations / coach_modules / sounds / sound_albums likewise
  • assets: pre-upload to R2 / directus_files, reference by UUID
  • set status='published' to go live (draft otherwise)
  • Save-time gate: the coach-content-validate hook (extension at naluma-directus/extensions/hooks/coach-content-validate/) rejects a save whose session_card.session_id is not a known SessionKind, whose session_card.session_slug (when supplied) does not resolve to a content.sessions row, or whose sound_card ref / module ref does not resolve to an existing content.* row. Create referenced rows (drafts) first.
  • Legal pages: NOT publishable via MCP — legal_pages and legal_pages_translations are intentionally excluded from the Content Editor (MCP) policy (defense-in-depth per OpenSpec change tighten-editorial-security).

Validate before publish

Validate the content body against the Directus schema for its typenaluma-directus/schemas/{session-content,coach-content}/<type>.schema.json, or the naluma-directus/authoring-docs/reference-manifest.json (confirmed present). The naluma-app-editor-copilot:publish skill now performs this: it self-validates the body against the Directus schemas (incl. coach items[]) as an offline pre-flight gate — the coach-content-validate hook (fixed in naluma-directus#90) is the server-side backstop — uploads the hero image via REST /files, and upserts the parent + *_translations row(s) at status: published after a dry-run + explicit confirm. It never deletes (the role is CRU-only); re-publish is an upsert by directus_id/slug.

Smoke-test (dev)

To verify the publish path against dev Directus (cmsdev.naluma.space): 1. Publish a throwaway draft coach_modules row (status: draft, an obvious mod-smoketest-DELETEME slug) via the Directus MCP; read it back. 2. Attempt a coach_conversations draft whose session_card.session_slug does NOT resolve to any content.sessions row — confirm the coach-content-validate hook rejects the save (proves the gate). 3. Delete the throwaway row(s); confirm they are gone.

Result (2026-06-05, dev cmsdev.naluma.space, role Content Editor (MCP)):

  • Step 1 — create + read: PASS. Created a draft coach_modules row (mod-smoketest-deleteme, type: pool, status: draft) via the Directus MCP and read it back. Note: the slug field rejects uppercase — slugs must be lowercase-hyphenated.
  • Step 2 — gate rejection: FAIL (gate did not fire). A session_card with a valid session_id (breathing) but a non-resolving session_slug was accepted — via both realistic publish paths: (a) a nested coach_conversations create with translations[].items, and (b) a direct coach_conversations_translations create. The bad card persisted verbatim in both cases. The coach-content-validate hook does not gate the live MCP publish path. Root cause (high confidence, from naluma-app/supabase/directus/snapshot.yaml
  • the hook source): the hook registers content.coach_conversations.items.create, but (1) the live collection is the unprefixed coach_conversations, and (2) the items[] JSONB lives on coach_conversations_translations, not the parent — so the hook should bind to coach_conversations_translations.items.create (and coach_modules_translations.items.create) and read payload.items / payload.variants there. The directus repo's unit/integration tests call the handler directly, so they don't catch the event-name mismatch. Owner: naluma-directus (editorial CMS runtime) — filed as a follow-up; do not fix from this repo.
  • Step 3 — delete: BLOCKED. The Content Editor (MCP) identity returns "Delete actions are disabled" — confirms the documented CRU-only role. Throwaway draft rows must be deleted manually in the admin UI (they are status: draft, so invisible to the app via v_published_*).
  • Incidental: coach_conversations.opening_module_id carries a stale default UUID that triggers an FK violation on parent create unless overridden — a dev-data quirk, not a rails defect.