Content OS

Admin Console — App Flows

Where the work happens. 43 screens, 113 API routes, four background workers.

Code: app/ · Port: 3000 · Access: admin only Back to apps index · Marketing OS


Who uses it

Content teams, reviewers and admins working inside one tenant. Everything is tenant-scoped by a tenantId and owned by a customerId; isolation is enforced in application code via an owner filter, not by the database.


Sign in

email + password
      |
rate limit: 10 attempts per IP        <- checkRateLimit, before any lookup
      |
content_admins  ->  verify salted hash
      |
HS256 token issued as a cookie
      |
admin-only console

Standalone auth — the app owns its own credentials and sessions. No refresh tokens, no revocation. Fails closed if JWT_SECRET is unset.

In dev you can skip it: ALLOW_DEV_AUTH=true plus CONTENT_ENGINE_DEV_ADMIN_ID, with NODE_ENV not production.


The five things you actually do

1. Set the tenant up

One-time, then rarely touched. Sources, audience personas and SEO keywords feed discovery; guidelines and the compliance rulebook constrain everything generated afterwards.

ScreenSets
SourcesNews sources, crawl config, search discovery
AudiencePersonas — matched deterministically against every article
KeywordsSEO inventory, plus gap analysis into blog topics
GuidelinesBrand voice, avoid list, the blog master prompt
Compliance IntelligenceThe AI reviewer's rulebook, up to 40 authored rules
SettingsCMS integration — credentials encrypted, admin-only to write

2. Find something worth writing about

cron: daily morning sweep  ->  /api/internal/news-fetch
      |                        (bearer secret, fails closed)
queue a durable fetch run per enabled tenant
      |
news-fetch worker drains it
      |
crawl sources + optional search-first discovery
      |
isArticleShapedUrl gate        <- rejects landing and hub pages
      |
rows land: enrichmentStatus "pending"
      |
feed-enrichment worker (~2s poll, one item at a time)
      |
summary - category - geo - keywords - scores      [Discovery Agent]
      |
Discovery feed, browsable

The same sweep also refreshes competitor reels per enabled tenant, unawaited — it is a paid scrape that can run for minutes.

3. Make a script

Make Content -> Daily Studio
      |
fetch fresh pieces, auto-select up to 3
      |
set audience - platform - category
      |
POST /scripts/generate   ->  content_generation_jobs
      |
generation worker: concurrency 2, stale jobs reclaimed after 60s
      |
hook formula enforced + cooldown against recent openings
      |
versions - regenerate - scene patch - history
      |
approve  ->  calendar

Stops at the calendar. Nothing posts.

4. Make a blog

queue a news item
      |
autoProcessBlogQueueItems     <- background, unattended
      |
draft -> quality repair -> SEO bolding -> ZeroGPT humanize -> blocked-phrase scan
      |
state: "ready"                <- STOPS HERE
      |
[human] Compliance -> acknowledge high-severity findings
      |
[human] Send to CMS           <- 409 at the route while the gate is unclear

Two human gates, both deliberate. The compliance verdict marks evidence phrases inline in the editor; an edit bumps the draft version and re-gates delivery, and Send re-runs the agent once when the verdict is stale so delivery is judged against the text actually going out.

From a marked phrase, Ask AI to fix returns a block-level Before/After in the chat rather than a whole rewritten article; accepting it clears that finding for as long as the flagged phrase stays gone. Not an issue additionally teaches a learned exception, so the same judgement is recognised on the next draft instead of being re-argued — as a badge and a one-click resolve, never as a suppressed finding, and never by editing the authored rulebook.

5. Review and govern

ScreenJob
ApprovalsTeam-lead queue of work items
Compliance ReviewPending acknowledgements across Discovery, in one place
Compliance ExceptionsJudgements reviewers already made, learned from dismissals. Arm one and the agent is told about it
Content GovernanceAdmin toggles that gate CMS delivery and scheduling
MembersRBAC — who holds which role and permission
Page VisibilityWhich nav pages a tenant sees at all

Intelligence, running underneath

  • Instagram learning — OAuth connect, then a cron sync (/api/internal/instagram-sync) feeding reel analysis and the v2 learning profile. Supplies hook formulas back into script generation.
  • Competitor analysis — own profile in the learning store, competitors in ops_ca_*, bridged by buildComparison behind an 8s timeout so a slow Instagram call can never blank the page.

What runs without anyone clicking

Four in-process workers, all started by app/src/instrumentation.ts:

WorkerDoes
GenerationDrains content_generation_jobs, concurrency 2
News fetchDrains queued fetch runs
Feed enrichmentEnriches one pending feed item at a time
Creator statsRefreshes creator platform stats

Plus two Coolify crons posting to /api/internal/* with bearer secrets. Those routes bypass the session gate and authenticate on their own secret, failing closed when it is unset.


Navigation

Five groups, split by what you are doing rather than by permission:

Produce (Make Content, Hooks) · Research (Discovery, Sources, Keywords, Audience) · Publish (Drafts, Blogs, Calendar, Approvals) · Intelligence (Learning, Competitor Analysis) · Guardrails (Guidelines, Compliance Intelligence, Compliance Review, Configs, Members)

Each page declares the permission it needs, so visibility and access stay in one definition (lib/nav-pages.ts).


Where it stops

  • Nothing publishes to social. Scripts reach the calendar; posting is manual.
  • Only blogs reach an external system, and only on a human click.
  • Scripts get no AI compliance review — the deterministic keyword scan only.
  • No customer data. Everything here is content production; the lifecycle programs in Program have nothing to run on.
Source: roadmap/apps/admin-app.md