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.
| Screen | Sets |
|---|---|
| Sources | News sources, crawl config, search discovery |
| Audience | Personas — matched deterministically against every article |
| Keywords | SEO inventory, plus gap analysis into blog topics |
| Guidelines | Brand voice, avoid list, the blog master prompt |
| Compliance Intelligence | The AI reviewer's rulebook, up to 40 authored rules |
| Settings | CMS 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.
5. Review and govern
| Screen | Job |
|---|---|
| Approvals | Team-lead queue of work items |
| Compliance Review | Pending acknowledgements across Discovery, in one place |
| Content Governance | Admin toggles that gate CMS delivery and scheduling |
| Members | RBAC — who holds which role and permission |
| Page Visibility | Which 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 bybuildComparisonbehind 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:
| Worker | Does |
|---|---|
| Generation | Drains content_generation_jobs, concurrency 2 |
| News fetch | Drains queued fetch runs |
| Feed enrichment | Enriches one pending feed item at a time |
| Creator stats | Refreshes 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.