Course Video Manager
August 23, 2026 · View on GitHub
A tool for managing course video publishing workflows — editing metadata, generating descriptions, creating thumbnails, and posting to social platforms.
Repository layout
A Turborepo monorepo over the pnpm workspace:
| Directory | What it is |
|---|---|
apps/local | The application as it runs on the author's machine: the React Router app, the Video Editor, the Diagram Playground, the Publish flow, ffmpeg, OBS, and the cvm CLI |
packages/core | The domain database — the Drizzle schema, the DrizzleService and every db-* operations service. Every piece of SQL in the repo lives here |
packages/overlay-renderer | The standalone Remotion renderer for every overlay content-kind — subtitles, the CTA, and Definition Cards — with its own toolchain |
packages/core has no filesystem access, no child_process and no git
coupling, so it can be deployed as well as run locally. pnpm lint:boundaries
enforces that — anything that needs a machine is injected from apps/local
(see packages/core/services/diagram-thumbnail-store.ts for the shape).
.env lives at the workspace root: one file for the whole monorepo, which is
also where cvm looks for it (apps/local/app/cli/env.ts).
Commands
Run these from the workspace root; Turborepo fans them out and re-runs only what changed.
| Script | Description |
|---|---|
pnpm typecheck | Typecheck every package |
pnpm test | Run every suite once |
pnpm test:watch | Run every suite in watch mode |
pnpm lint:boundaries | Enforce the package boundaries |
pnpm dev | Start the local application |
pnpm build | Build the local application |
Each of these filters out @cvm/overlay-renderer: it ships its own
toolchain (Remotion, and a Chromium download) and has never been part of the
application's checks. Run it with pnpm --filter @cvm/overlay-renderer.
Deploys
Vercel gets one project per deployable directory, each with its own Root
Directory, and relies on Vercel's built-in unaffected-project skipping to
decide what to deploy. There is deliberately no Ignored Build Step:
turbo-ignore is deprecated, and native skipping does not consume a concurrent
build slot. If a custom step is ever needed it is turbo query affected.
Database migrations
Schema changes are managed with drizzle-kit generate / migrate (versioned SQL files), not push. The schema, the migrations and the drizzle config all live in packages/core.
Making a schema change
- Edit
packages/core/db/schema.ts. pnpm db:generate— creates a new numbered.sqlfile underpackages/core/db/migrations/.- Commit it, then
pnpm db:migrate— run by hand, againstDIRECT_DATABASE_URL— before or as part of deployingapps/remote. Applying migrations used to be the deploy's job exclusively; it moved to a manual step because that ran on every Vercel build, previews included, and could land an unmerged migration on the production schema. Seeapps/remote/README.mdand ADR 0026.
Migrations are additive-only: no dropped or renamed columns without a two-step release. A cvm invocation may be in flight while a deploy lands, and it is the additive rule — not the version gate — that keeps that from breaking. The version gate refuses the box's next command, naming both migration counts and telling it to pull (packages/core/rpc/schema-version.ts).
First-time setup on an existing database
If the database was originally created via drizzle-kit push and has never run migrations:
pnpm db:baseline
This registers the 0000 baseline migration as already-applied so the next pnpm db:migrate won't replay the initial CREATE TABLE statements.
Scripts
| Script | Description |
|---|---|
pnpm db:generate | Generate a new migration from schema changes |
pnpm db:migrate | Apply pending migrations by hand (deploy no longer does this) |
pnpm db:baseline | Mark the 0000 baseline as applied (one-time setup) |
pnpm db:studio | Open Drizzle Studio |
Zapier Webhook Setup (Buffer Integration)
The app uses a Dropbox → Zapier → Buffer pipeline to post videos to social media. When you click "Post to Buffer" in the app, it:
- Copies the video file into a local Dropbox folder
- Waits for Dropbox to sync the file to the cloud
- Sends a webhook to Zapier with the caption and file path
- Zapier finds the file in Dropbox and adds it to your Buffer queue
Prerequisites
- Dropbox desktop client installed and running (the app uses
dropbox filestatusto poll sync status) - Buffer account connected in Zapier
- Zapier account with access to the Webhooks by Zapier and Buffer integrations
Environment Variables
| Variable | Description |
|---|---|
BUFFER_POSTS_PATH | Local path to a folder inside your Dropbox directory where video files are copied before posting (e.g. ~/Dropbox/buffer-posts) |
ZAPIER_BUFFER_WEBHOOK_URL | The webhook URL generated by your Zapier Zap (see below) |
AI_HERO_BASE_URL | Base URL for the AI Hero instance (e.g. https://www.aihero.dev). Required for AI Hero posting integration. |
Creating the Zapier Zap
Step 1: Create a "Webhooks by Zapier" trigger
- Create a new Zap in Zapier
- For the trigger, choose Webhooks by Zapier
- Select Catch Hook as the trigger event
- Copy the generated webhook URL
- Set it as the
ZAPIER_BUFFER_WEBHOOK_URLenvironment variable in your app
The webhook receives a JSON payload with this shape:
{
"caption": "Your post caption text",
"dropboxFilePath": "/full/path/to/buffer-posts/video.mp4"
}
Step 2: Add a "Dropbox: Find File" action
- Add an action step and choose Dropbox
- Select Find File as the action event
- Configure it to look up the file using the
dropboxFilePathvalue from the webhook payload
Step 3: Add a "Buffer: Add to Queue" action
- Add another action step and choose Buffer
- Select Add to Queue as the action event
- Map the media field to the Dropbox file URL from Step 2
- Map the text field to the
captionvalue from the webhook payload
Step 4: Test and enable
- Use the app to trigger a test post so Zapier can capture a sample webhook payload
- Walk through each step to verify the data mapping is correct
- Turn on the Zap