> ## Documentation Index
> Fetch the complete documentation index at: https://meganharrisonconsulting.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Training Docs Architecture

> What's already built in the internal training system, and what's greenfield.

**Audience:** anyone (Brandon, a contractor, a new agent) about to build on the internal
training system. Read this before writing any training-related code.

**Last verified:** 2026-07-18 against live migrations + `TABLE-LIST.md` row counts.

***

## The one-paragraph version

We have a **built, live, in-use system for authoring and publishing app training
content**. An admin writes a task-based training doc (title, summary, ordered steps,
screenshots/video), it lives in four Postgres tables, and it surfaces in two places: the
in-app reader at `/knowledge/app/*`, and — on an explicit publish action — as MDX pages
on the Mintlify docs site. There are 90 docs, 118 steps, and 124 assets in there today.

What we **do not** have is a learning-management system. Nothing tracks a *person*.
There are no courses, enrollments, assignments, quizzes, scores, completions, due dates,
or certificates. Every one of those is greenfield, and all of it is **additive** — it
builds on top of what exists, it does not replace it.

That distinction is the whole point of this document:

> **Content authoring: built. Learner tracking: not built.**

***

## The four tables

All in the **PM APP** Supabase project (`lgveqfnpkxvzbnnwuled`), domain `support`,
status `live`.

```
training_docs ──┬── training_doc_steps      (ordered instructions)
                ├── training_doc_assets     (screenshots / images / video)
                └── training_doc_relations  (doc → doc graph)
```

### `training_docs` — one row per task/article (90 rows)

| Column                                                                               | Notes                                                                             |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `id` uuid PK, `slug` unique                                                          |                                                                                   |
| `title`, `summary`, `body_markdown`                                                  | the content                                                                       |
| `audience`                                                                           | `internal \| client \| subcontractor \| admin`                                    |
| `status`                                                                             | `planned \| draft \| in_review \| approved \| published \| archived`              |
| `tool_category`, `tool_module`, `task_key`                                           | feature grouping; `task_key` has a unique partial index                           |
| `qa_status`                                                                          | `not_tested \| passing \| failing \| needs_update` + `qa_last_run_at`, `qa_notes` |
| `source_route`                                                                       | the app route the doc documents                                                   |
| `target_collection`, `published_doc_path`, `last_published_at`, `last_publish_error` | publish bookkeeping                                                               |
| `created_by`, `updated_by`                                                           | → `user_profiles(id)`                                                             |

### `training_doc_steps` — ordered instructions (118 rows)

`training_doc_id` (CASCADE), `step_order`, `title`, `instruction_markdown`,
`expected_result`, `source_url`, `action_metadata`, `screenshot_asset_id` → assets.

### `training_doc_assets` — media (124 rows)

`storage_bucket` (default `documents`), `storage_path`, `asset_type`
(`screenshot | image | video`), `caption`, `alt_text`, `step_order`.
Unique on `(storage_bucket, storage_path)`.

### `training_doc_relations` — the training map (18 rows)

`source_doc_id` → `target_doc_id`, `relation_type` (`related | prerequisite | next`),
`sort_order`. Constrained against self-reference and duplicates.

**Migrations** (`supabase/migrations/`):
`20260627022000_create_training_docs.sql`,
`20260629204500_allow_training_doc_image_assets.sql`,
`20260630000500_create_training_doc_steps.sql`,
`20260630193000_allow_training_doc_video_assets.sql`,
`20260630210000_extend_training_docs_relations_tool_qa.sql`.

***

## RLS — read this before you build any employee-facing feature

All four tables are **admin-only** via `current_is_app_admin()`, plus a `service_role`
full-access policy. A normal employee **cannot read `training_docs` directly.**

The in-app reader gets around this by using a **service client** on the server:

```ts theme={null}
// frontend/src/app/(main)/knowledge/app/page.tsx
const trainingDocs = await listPublishedTrainingDocs(createServiceClient());
```

That is fine for read-only published content rendered server-side. It is **not** a
pattern to copy for anything per-user — the moment a feature needs "what has *this
person* done," it needs real RLS policies keyed to the user, not a service-role bypass.

**Any employee-training feature therefore needs a deliberate RLS decision first.**
See `docs/architecture/RLS-ACCESS-LEVEL-DECISION.md`.

***

## How content flows

```
   Admin authors                Stored                    Consumed
   ─────────────                ──────                    ────────
   /admin/training-docs   →   training_docs        →   /knowledge/app/*
   (editor UI)                training_doc_steps       (in-app reader,
                              training_doc_assets       reads the DB via
   Playwright recorder    →   training_doc_relations    service client)
   (scripts/tutorials/)                              →   Mintlify docs site
                                                        (explicit publish
   AI step generation     →                             writes .mdx files)
   (/generate endpoint)
```

Two things worth internalizing:

1. **The database is the source of truth.** The published MDX is a *derived export*, not
   the runtime source. The in-app reader queries Postgres.
2. **Publishing is an explicit action, not a sync.** `POST /api/admin/training-docs/[docId]/publish`
   renders MDX, writes assets, and registers nav in `docs.json`. Nothing happens
   automatically on save.

***

## Where the code lives

| Layer                                         | Path                                                                          |
| --------------------------------------------- | ----------------------------------------------------------------------------- |
| Admin list / editor / detail / preview        | `frontend/src/app/(admin)/training-docs/`                                     |
| Training coverage map (by tool + QA status)   | `frontend/src/app/(admin)/training-map/`                                      |
| Table + map configs                           | `frontend/src/features/training-docs/`                                        |
| API (list, CRUD, publish, generate, assets)   | `frontend/src/app/api/admin/training-docs/`                                   |
| Server lib + publish pipeline (tested)        | `frontend/src/lib/training-docs/{server,docs-site,types,constants}.ts`        |
| React Query hook                              | `frontend/src/hooks/use-training-docs.ts`                                     |
| In-app reader surface                         | `frontend/src/app/(main)/knowledge/app/` + `frontend/src/features/knowledge/` |
| Playwright tutorial recorder + workflows      | `scripts/tutorials/`                                                          |
| Video walkthrough skill (Playwright + ffmpeg) | `.claude/skills/training-video/`                                              |

Nav entries: `frontend/src/lib/navigation-config.ts` (Task Training, Training Docs,
Training Map, Knowledge Sources).

***

## Three things named "training" that are NOT this

Do not confuse these — they share a word and nothing else.

| Thing                                                                   | What it actually is                                                                                                   |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Task Training** (`(admin)/task-training/`)                            | AI few-shot tuning. Reads `ai_task_feedback` for good/bad signals on AI-generated tasks. About the model, not people. |
| **Agent learning** (`agent_learnings`, `self_learning_feedback_events`) | Agent self-improvement feedback loop.                                                                                 |
| **`user_profiles.onboarding_completed_at`**                             | A single timestamp. Not a curriculum, not progress tracking.                                                          |

***

## What is confirmed absent

No table and no code anywhere for: `courses`, `lessons`, `modules`, `quizzes`,
`curriculum`, `enrollments`, `certifications`, `learning_paths`,
`training_assignments`, `training_completions`. No `/academy`, `/university`,
`/learn`, or `/courses` route.

***

## If you're adding employee training — build it as a layer, not a rewrite

The existing schema is **content**. What's missing is **learner state**. Add it
alongside; do not restructure `training_docs`.

The seam is already there: `training_docs.task_key` and `training_docs.id` are the
natural join targets, and `user_profiles.id` is the person.

A progress layer would look roughly like:

```
user_profiles ──┐
                ├── training_assignments  (who must do what, by when)
training_docs ──┘
                └── training_completions  (who did what, when, score/attestation)
```

Before writing the migration, settle these — they're the decisions that are expensive to
change later:

1. **Unit of assignment.** A single doc? Or a new `training_paths` grouping (which
   `training_doc_relations` with `relation_type='prerequisite'` already half-models —
   check whether that's enough before adding a table).
2. **RLS model.** Employees read published content and read/write *only their own*
   progress. This is a real policy design, not a service-client bypass.
3. **Content read access.** `training_docs` is admin-only today. Either relax the read
   policy for `status='published'`, or keep server-side rendering as the only read path.
   Pick one deliberately.
4. **Completion semantics.** Self-attested checkbox, quiz score, or observed-in-app?
   These have very different schemas and very different levels of effort.

***

## Ground rules for building here

* **Reuse before you add.** `REUSE-FIRST-GATE.md` applies. The editor, publish pipeline,
  asset handling, and reader surface all exist and are tested — compose them.
* **Table pages use `UnifiedTablePage`.** See `.claude/rules/TABLE-PAGE-GATE.md`.
* **Detail pages use the `alleato-detail-page` skill.** See `.claude/rules/DETAIL-PAGE-GATE.md`.
* **Run `npm run db:types` before writing any query**, and confirm columns against
  `frontend/src/types/database.types.ts`.
* **Any new table must be added to `docs/architecture/tables.yaml`**, then run
  `npm run db:inventory` to regenerate `TABLE-LIST.md`. A drift check fails otherwise.
* **Branch + PR.** Never commit to `main`. See CLAUDE.md "Git Workflow".

***

## Related planning docs

Historical context and prior decisions live in `docs/ops/tasks/`:
`2026-06-27-brandon-email-training-queue.md`,
`2026-06-29-ai-training-doc-generation.md`,
`2026-06-30-repeatable-training-doc-skill.md`,
`2026-06-30-training-docs-app-knowledge.md`,
`2026-07-01-training-docs-control-plane-fields.md`,
`2026-07-17-training-docs-skill-gates.md`.
