Tactical Media Broadcast Hub — initial release

Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
This commit is contained in:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

130
infra/occupymedia/DEPLOY.md Normal file
View file

@ -0,0 +1,130 @@
# Deploy — occupymedia.cc
**Storage phase-one addendum:** the current deployment uses separate app/chat/
PeerTube CTs. New files/docs work is an independent, undeployed scaffold;
see [files runbook](files/README.md) and [storage contracts](../../docs/occupymedia-storage.md).
The older one-box instructions below do not deploy that stack. Live conduwuit
v0.5.0 still has local media; its volumes require backup. R2 durability alone
does not back up media or the metadata needed to resolve it.
One box, everything behind the Cloudflare tunnel (already provisioned: zone
`occupymedia.cc`, tunnel `occupymedia` with ingress app/api/tube/chat, R2
buckets `occupymedia-media-encrypted`, `-peertube-videos`, `-peertube-playlists`,
`-matrix-media`). No inbound ports. All local ports bind to 127.0.0.1.
## WP3 release on the app CT (150)
Use `docker compose -f docker-compose.app.yml` in this directory. Before
syncing source, create and verify an owner `pg_dump` in
`/opt/tactical-media-broadcast-hub/backups/`. Preserve the previous source for rollback.
Both `DATABASE_URL` and `SCHEDULER_DATABASE_URL` in the CT `.env` must connect
as `sp_app`, without superuser or BYPASSRLS privileges. Provision its password
with owner access and keep real values only in the CT `.env`. Keep the
existing `SUPERPOSTER_KEK`; scheduler inherits it and `TEMPORAL_ADDRESS`.
Stop the old scheduler, then run the new API migration CLI offline with
`SUPER_DATABASE_URL` set to the owner URL:
`pnpm --filter @tactical-media-broadcast-hub/api exec tsx src/db/migrate.cli.ts --dir migrations`.
The CLI skips recorded migrations; 019 tracks resumable transactional, batch,
and concurrent-index phases. Confirm 018 and 019 in `schema_migrations` and
check `assertPublishingSchema` using the runtime URL before starting new code.
Build `api scheduler web`, then run `up -d --no-deps api`, followed by
`scheduler` and `web`. Verify API health/readiness, invite regressions, an
existing username/password login, scheduler readiness and Temporal connectivity,
and the public web app. Defer production publish tests until explicitly requested.
For rollback, restore source from the previous commit, re-sync, rebuild and
restart the three services; do not undo migrations automatically.
## 0. Prerequisites on the box
- Debian 12 / Ubuntu 24.04, 4+ vCPU, 8 GB RAM, 100 GB disk
- Docker Engine + compose plugin (`curl -fsSL https://get.docker.com | sh`)
- git
## 1. Get the code + secrets
```bash
git clone git@gitlab.example.com:mediatools/tacticalmediahub.git
cd tactical-media-broadcast-hub/infra/occupymedia
cp .env.example .env
# Fill: CLOUDFLARE_TUNNEL_TOKEN, R2_ACCESS_KEY, R2_SECRET_KEY (dashboard → R2 →
# Manage API Tokens → Object Read & Write, scoped to the occupymedia-* buckets),
# POSTGRES_PASSWORD, PEERTUBE_DB_PASSWORD, S3_KEK (openssl rand -hex 32 — ONE TIME,
# it wraps every stored blob; lose it = archive unreadable)
# peertube.env: set both PEERTUBE_DB_PASSWORD / POSTGRES_PASSWORD lines to the
# value from .env, and set a registration/SMTP sender if you have them.
```
## 2. Build + start the core
```bash
docker compose build api scheduler web media-worker
docker compose up -d postgres redis temporal peertube-postgres
```
## 3. Apply API migrations (ordered, once)
```bash
cd ../..
for f in apps/api/migrations/*.sql; do
docker compose -f infra/occupymedia/docker-compose.yml exec -T postgres \
psql -U "$(grep -oP '(?<=POSTGRES_USER:)\S+' infra/occupymedia/docker-compose.yml | head -1 | tr -d '${:-occupymedia}')" \
-d occupymedia -v ON_ERROR_STOP=1 < "$f" || { echo "FAILED at $f"; break; }
done
```
(If psql errors on an already-applied migration — re-runs are idempotent-safe
for most files; check the failing file before continuing.)
## 4. Bring everything up
```bash
docker compose -f infra/occupymedia/docker-compose.yml up -d
docker compose ps # everything Up/healthy
```
## 5. First-boot steps
1. **Tunnel**: `docker compose logs cloudflared` — should show 4 connections
registered. Then `https://app.occupymedia.cc` (control room), `/healthz` on
api., tube., chat. should all answer.
2. **PeerTube admin**: `docker compose logs peertube 2>&1 | grep -i -A3 password`
— random root password printed ONCE. Log in at tube.occupymedia.cc, change
it, then run the brand sync (`docs/peertube-setup.md`) and create the live
channel.
3. **Matrix admin**: register the first account at chat.occupymedia.cc with the
token from conduwuit.toml, then set `allow_registration = false` and
`docker compose restart conduwuit`. Configure Element as client
(`docs/matrix-setup.md`).
4. **Telegram bridge** (optional now): `docker compose --profile bridges run --rm
mautrix-telegram` → interactive login with the bot/user token; then
`docker compose --profile bridges up -d mautrix-telegram`.
## 6. Live ingest (deferred)
RTMP/SRT cannot traverse the tunnel. Options documented in the session notes:
tiny relay VPS + WireGuard to this box (recommended; keeps the home IP out of
DNS), or direct port-forward. Until then the 24/7 channel runs playlists/VOD
(media-worker pushes to PeerTube internally over the docker network).
## 7. Operations
- **Backup**: `docker compose exec postgres pg_dump -U occupymedia occupymedia |
gzip > backup-$(date +%F).sql.gz` + `tar` the named volumes (pgdata,
peertube_pgdata, conduwuit_data). Media itself is in R2 (durable) — KEK
backup matters more than volume backup.
- **Update**: `git pull && docker compose build api scheduler web media-worker &&
docker compose up -d` (migrations: repeat step 3 for NEW files only).
- **Logs**: `docker compose logs -f --tail=100 <service>`.
## Known simplifications (first-production-pass)
- observability (otel/prometheus/grafana) not included — add from
infra/docker-compose.yml services when needed
- single app instance per service (no horizontal scaling — intended at this size)
- file-sharing/collab layer: oCIS S3NG + optional Collabora scaffold in
`files/compose.yml`; deployment and Matrix migration remain separate prerequisites