Apps — Index
Two shipped applications, one database, one pipeline running between them.
| App | Code | Who | Size |
|---|---|---|---|
| Admin Console | app/ | Content teams, reviewers, admins | 43 screens, 113 API routes, 4 workers |
| Client Portal | client-app/ | The client, reviewing work | 1 screen, 5 API routes |
A third app, the roadmap viewer (roadmap-app/), renders these docs. It has no
pipeline role.
How they fit together
The client portal is not a separate product — it is the human approval gate in the middle of the admin app's pipeline.
ADMIN CLIENT ADMIN
───── ────── ─────
sources
|
fetch + enrich ──────▶ content_news_items
| |
| Discovery tab
| approve (Y) / skip (N)
| |
| content_script_queue
| content_generation_jobs
| { origin: "client" }
| |
generation worker ◀────────────────────┘
|
└──────────────────────▶ content_scripts
|
Scripts tab
approve / send back
|
content_scripts.status ──────▶ calendar
|
Calendar tab ◀───────────────────────┘
(read-only)
They meet in MongoDB, never over HTTP. The portal reads and writes the same
content_* collections directly; there is no API call between the two apps.
What differs
| Admin | Client | |
|---|---|---|
| Auth | Email + password against content_admins | One env-provided credential, CLIENT_* |
| Session cookie | Admin cookie | content_engine_client_token, 15 days |
| Tenants | Many, switchable | One, resolved from the session |
| Blogs | Full studio, CMS delivery | Not visible at all |
| Scripts | Write, refine, version, schedule | Read four sections, approve or reject |
| Production notes | Visible | Stripped at the data layer before they reach the browser |
That last row is worth keeping: toClientVersions() drops on-screen text,
b-roll, editing notes and scores at the data boundary rather than hiding them in
the UI, so a future component change cannot leak them.
Shared and not shared
Shared: MongoDB, JWT_SECRET, the content_* collection schema, and the
tenant ownership chain (customerId is the authorization source of truth in
both).
Not shared: code. script-sections.ts exists as a deliberate hand-copied
read-only fork in the portal, because the two apps build from separate
Dockerfiles with no workspace linkage. Index creation is owned by the admin app
alone.
The gap between them
The client can reject, but cannot say why. Skip and send-back both carry no reason, so the admin side learns that something was rejected and never what was wrong with it — which also means nothing downstream can learn from it. See Learning Agent, whose whole job would be reading exactly that signal.