The acampada26s.net mark as is (two tents), with a cable coming out of the bottom tent's door to a radio antenna. Colour (blue/orange) and white versions, SVG and 1024px PNG. |
||
|---|---|---|
| .github/workflows | ||
| apps | ||
| docs | ||
| infra | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| eslint.config.mjs | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
| turbo.json | ||
T-HUB

Copia de trabajo del Tactical Media Broadcast Hub para la red de acampadas: acampada26s.net, tomalacalle.net y takethestreet.net. El despliegue en uso es app.occupymedia.cc.
Seguimos la rama main de upstream y mandamos allí los cambios que les sirvan.
Lo propio de T-HUB va en este repo. Para revisar y validar los cambios, ver
docs/T-HUB-VALIDACION.txt.
Cambios respecto a upstream:
- Instagram registrado en conexión y publicación; claves de la app de Meta solo en el entorno del servidor.
- Panel Outreach para escribir por privado a mano, contacto a contacto, con seguimiento.
- RLS forzado en
media_assets(migración 042). - Estilo de acampada26s.net en la web: fondo blanco, tinta negra, neón
#00e5ffen sombras y foco, títulos en Anton. Logo de tres tiendas cableadas a un switch con antena, dibujado como el de la acampada (docs/brand/).
| Remoto | URL |
|---|---|
origin |
https://gitea.laenre.net/hacklab/T-HUB.git |
upstream |
https://gitlab.moxexley.com/mediatools/tacticalmediahub.git |
git fetch upstream && git merge upstream/main
Arquitectura
1. Una cuenta
Una persona conecta una cuenta del colectivo una sola vez. A partir de ahí redacta, alguien aprueba y el Hub publica. La contraseña de la red social no pasa nunca por el Hub: la red devuelve un token y el Hub lo guarda cifrado.
flowchart LR
P([Persona]) -->|1. Conectar| H[T-HUB]
H -->|2. Pantalla de permiso| R[(Red social)]
R -->|3. Token| H
P -->|4. Redacta| H
H -->|5. Aprobación| A([Otra persona del colectivo])
A -->|6. Visto bueno| H
H -->|7. Publica a su hora| R
2. Varias cuentas compartidas
Cada colectivo tiene sus cuentas y sus reglas de aprobación. Un borrador puede ir a varias cuentas a la vez: el Hub crea una copia por destino y cada una pasa la regla de su cuenta (automática, una persona, mayoría o consenso). Lo que deba salir en toda la red sube a la cola del colectivo padre.
flowchart TB
subgraph TTS[TAKE THE STREET]
Q[Cola común] --> IG1[Instagram internacional]
Q --> M1[Mastodon @takethestreet]
end
subgraph S26[ACAMPADA 26S]
D[Borrador] --> IG2[Instagram @acampada26s]
D --> M2[Mastodon @26s]
D --> T2[Telegram acampada26s]
end
subgraph SUB[Acampadas locales]
B[Barcelona] --> IG3[Instagram propio]
S[Sevilla] --> IG4[Instagram propio]
end
S26 -->|Sube para toda la red| Q
SUB -->|Sube a 26S| D
IG2 -.->|Regla: 2 aprobaciones| G{{Aprobación}}
M2 -.->|Regla: automática| G
3. Parte técnica
La web solo habla con la API. La API guarda el estado en Postgres con aislamiento por colectivo (RLS), los tokens van en un vault cifrado y el envío programado lo hace el scheduler con Temporal. Cada red es un conector; fuera de los conectores no hay código que distinga redes.
flowchart LR
subgraph Cliente
W[apps/web<br/>Next.js]
end
subgraph Control
API[apps/api<br/>NestJS]
SCH[apps/scheduler<br/>Temporal worker]
REG[ConnectorRegistry<br/>packages/connector-*]
end
subgraph Datos
PG[(PostgreSQL<br/>RLS por colectivo)]
RD[(Redis<br/>estado OAuth 10 min)]
V[Vault<br/>AES-256-GCM + KEK]
S3[(S3 / R2<br/>medios cifrados)]
end
subgraph Medios
MW[apps/media-worker<br/>FFmpeg]
PT[PeerTube<br/>tube.occupymedia.cc]
end
subgraph Redes
MA[Mastodon API]
IG[Graph API<br/>Instagram]
TG[Telegram Bot API]
end
W -->|HTTPS + sesión| API
API --> PG
API --> RD
API --> V
V --> PG
API -->|connect / identity| REG
SCH -->|publish| REG
SCH --> PG
REG -->|providerFetch<br/>control de salida| MA
REG --> IG
REG --> TG
MW --> S3
MW -->|RTMP| PT
Conectar una cuenta (OAuth con state de un solo uso y PKCE donde la red lo admite):
POST /connections/authorize: la API guarda en Redis, cifrado, el estado de la petición y devuelve la URL de la red.- La persona acepta en la red y la red vuelve a
GET /connections/callback. - La API comprueba
state, cookie y permisos, canjea el código y guarda el token en el vault.
Estado por red
| Red | Conectar | Publicar | Notas |
|---|---|---|---|
| Mastodon | Sí | Texto e imágenes (con texto alternativo) | Sin claves de servidor; cada cuenta registra la app en su instancia. |
| Telegram | Sí | Texto e imágenes | Token del bot de BotFather. |
Sí, con INSTAGRAM_CLIENT_ID/SECRET |
No todavía | Meta descarga la imagen de una URL pública y los medios están cifrados: falta un endpoint temporal. Ver docs/oauth-connect.md. | |
| PeerTube | No | Directo sí | Falta el puente OAuth en tube.occupymedia.cc. |
| X | No | No | Sin conector. API de pago por uso. |
| TikTok | No | No | Fuera de alcance: la API exige auditoría y sin ella solo publica en privado. |
Panel Outreach (mensajes privados)
Lista de cuentas públicas (artistas, colectivos, medios) a las que escribir por
privado para pedir difusión. Cada contacto tiene su mensaje, se puede rellenar
con una plantilla ({name}, {handle}) y se marca enviado, respondido o
rechazado. Se importa en CSV:
name,platform,handle,profileUrl,category,language
Climáximo,bluesky,climaximopt.bsky.social,https://bsky.app/profile/climaximopt.bsky.social,clima,pt
El Hub no envía los mensajes. "Copy & open profile" copia el texto y abre el perfil; la persona lo pega y lo envía desde la propia red. Motivos:
| Red | Abrir un privado por API | Problema |
|---|---|---|
| No | La API solo deja responder a quien te escribió en las últimas 24 h. | |
| X | Sí, de pago | Mensajes en masa a desconocidos van contra sus normas de automatización: suspensión. |
| Mastodon | Sí (mención privada) | Las instancias silencian o bloquean cuentas que lo hacen en masa; puede caer la instancia entera. |
| Bluesky | Sí | Mismo criterio de spam; los chats de desconocidos suelen estar cerrados. |
El contador de la pantalla avisa a partir de 20 primeros mensajes al día por colectivo. Repartidlos entre varias personas y días, y personalizad cada uno.
Requisitos
Medido en el despliegue actual (ver docs/ops/2026-10-12-cloud-hosting-requirements.md). Los medios van a R2, así que el disco no crece con los vídeos.
| Nivel | CPU | RAM | Disco | Para qué |
|---|---|---|---|---|
| Mínimo | 4 vCPU | 8 GB | 160 GB SSD | Todo el stack, transcodificación fuera de la máquina |
| Recomendado | 8 vCPU | 16 GB | 240 GB SSD | Directos, transcodificación en la máquina, Collabora |
| Nodo de render (opcional) | 4+ núcleos o GPU con NVENC | 8 GB | 50 GB | Se une por Tailscale; nunca guarda claves |
Uso real hoy: unos 2,6 GB de RAM entre todos los servicios.
Ancho de banda:
| Uso | Necesario |
|---|---|
| Panel, publicación, APIs de redes | < 1 Mbps sostenido |
| Un directo saliente (1080p30 hacia PeerTube) | 6 Mbps de subida por directo; 3 Mbps a 720p |
| Espectadores | Los sirve Cloudflare desde la caché de HLS; el origen no escala con la audiencia |
| Subidas de vídeo | Picos de lo que dé la línea; límite de 2 GiB por archivo |
Coste orientativo: VPS de 30 €/mes + R2 de 1 a 5 $/mes. Cloudflare en plan gratuito.
Red y puertos
Entrada desde internet: ninguna directa. Todo entra por el túnel de Cloudflare; el nginx de borde solo acepta rangos de Cloudflare en 80/443.
| Puerto | Servicio | Exponer a |
|---|---|---|
| 443/tcp | nginx de borde | Solo rangos de Cloudflare |
| 80/tcp | nginx de borde (redirección) | Solo rangos de Cloudflare |
| 3000/tcp | API | Solo 127.0.0.1 |
| 3001/tcp | Web | Solo 127.0.0.1 |
| 9000/tcp | PeerTube | Solo 127.0.0.1 o Tailscale |
| 1935/tcp | RTMP de PeerTube | Solo Tailscale (worker de medios) |
| 6167/tcp | Matrix (conduwuit) | Solo 127.0.0.1 |
| 9200, 9980/tcp | oCIS, Collabora | Solo 127.0.0.1 |
| 5432, 6379, 7233 | Postgres, Redis, Temporal | Nunca fuera de la red de Docker |
Salida que hace falta: 443/tcp a las APIs de las redes y a R2, 7844/tcp+udp
para cloudflared, 41641/udp para Tailscale.
Docker publica puertos saltándose ufw. Para cerrarlos: publicar en 127.0.0.1:PUERTO
o en la IP de Tailscale, o añadir reglas en la cadena DOCKER-USER.
Seguridad y monitorización
Prácticas para cualquier despliegue. Los fallos de seguridad se comunican en privado a quien mantiene el código, no en issues públicas.
- Registro solo por invitación. Un registro abierto da a cualquiera un colectivo propio y permisos de Owner sobre él.
- Un secreto por servicio. La web y los workers no necesitan
SUPERPOSTER_KEK,S3_KEKniSESSION_SECRET. Claves distintas para el vault y para los medios. - FFmpeg siempre en sandbox (
NODE_ENV=production,MEDIA_SANDBOX=on), sin red y sin variables de entorno heredadas. - Cabeceras: CSP estricta, HSTS,
X-Content-Type-Options,Referrer-Policyen web y API. - Límites de peticiones en
/auth/*, invitaciones, OIDC y subidas públicas, porCF-Connecting-IP. - Metadatos fuera: quitar EXIF/GPS de fotos y vídeos antes de guardarlos o publicarlos.
- Copias cifradas (
ageo GPG) y fuera de la máquina; probar la restauración. - Auditoría inalterable: quitar
UPDATE/DELETEsobreaudit_logal rolsp_appy anclar fuera la cabeza de la cadena.
Qué vigilar (Prometheus + Grafana ya están en infra/; Grafana nunca con acceso anónimo):
| Señal | Alerta si |
|---|---|
401/403 en /auth/login por IP |
> 20 en 5 min |
| Registros nuevos / invitaciones canjeadas | Cualquiera fuera de lo previsto |
Eventos audit_log: member.*, connection.removed, group.deleted, outreach.removed |
Picos o actores raros |
connections en estado needs_credentials |
Cualquiera: token revocado o caducado |
| Errores 5xx de la API | > 1 % durante 5 min |
| Memoria de API y workers | > 80 % |
Cola de publicación failed/unknown |
Cualquiera |
| Logins al panel de Cloudflare y cambios de DNS | Siempre |
Desarrollo
corepack enable
pnpm install
pnpm exec turbo run build typecheck --filter=@tactical-media-broadcast-hub/api...
pnpm --filter @tactical-media-broadcast-hub/connector-instagram test
Los tests de integración de apps/api que usan Postgres se saltan si no hay
base de datos configurada.
Upstream: Tactical Media Broadcast Hub
Self-hosted multi-tenant social / video / livestream management platform with a generic connector system and a Virtual Channel / Auto Director subsystem.
Built for collectives that need to coordinate media across platforms without concentrating access in one person's hands. Every credential lives in an encrypted vault. Every editorial decision lands in a tamper-evident audit log. Every feature is multi-tenant with row-level security.
Features
- Publishing pipeline — drafts → group approval → schedule → fenced dispatch → receipts. Per-connection publish governance:
auto, one-admin, majority or consensus sign-off per account — boosts included. Multi-platform fan-out (Mastodon, Telegram, Bluesky, PeerTube, Facebook, Instagram, Threads, LinkedIn, Reddit, Twitch, Discord, Slack). DST-proof scheduling. - Files SSO — the Hub is the OIDC issuer for oCIS Drive: one sign-in, auto-provisioned personal drives, rotating refresh tokens that die with your Hub session (logout propagates).
- Column wall — live multi-platform feed columns, TweetDeck-style. Safe rendering (hostile-HTML tested). Per-source cooldown + adaptive backoff.
- Live Control Room — browser multiview, TAKE/CUT, preview/program, lower-thirds, scrolling chyrons, audio mixing, tally.
- Auto-Director — priority rules switch sources automatically so the stream never dies mid-action.
- Chat governance — bridged chats (Telegram/Twitch/Discord) into one overlay. Polls with configurable quorum. Approval gates on every post. Machine-readable reason codes.
- Distributed transcode workers — laptops and GPUs join as render workers over Tailscale. Per-job sandboxed FFmpeg (UID/seccomp/no-network).
- Archive — content-addressed encrypted S3, dedup, full-text search, CSV/JSON export. Deleted platform content stays citable.
- Analytics — month-partitioned snapshots, current + previous month with deltas, per-provider breakdown.
Quickstart
pnpm install
pnpm lint && pnpm typecheck && pnpm build
# infrastructure
docker compose -f infra/docker-compose.yml up -d postgres redis temporal minio otel-collector prometheus grafana
# everything incl. media stub + mock live source + mock RTMP sink
docker compose -f infra/docker-compose.yml --profile dev up -d
# apps
pnpm dev # api :3000, web :3001(adjust if colliding), scheduler, media-worker :3100
curl localhost:3000/healthz
curl localhost:3000/readyz
Documentation
| Section | Path |
|---|---|
| Engineering guide | docs/engineering-guide.md |
| Admin guide | docs/admin-guide.md |
| User manual | docs/user-manual-v2.md |
| Design docs | docs/design/ |
| Streaming architecture | docs/design/streaming-architecture.md |
| Deployment (occupymedia.cc) | infra/occupymedia/DEPLOY.md |
Deterministic live-source testing
pnpm --filter @tactical-media-broadcast-hub/media-worker soak -- --hours 8 --out
Security & trust model
Self-hosted, end to end — no corporate cloud, no accounts needed to run the system itself. Multi-tenant with row-level security; platform credentials in an encrypted vault the database never sees in plaintext; every editorial action in a hash-chained audit log; anonymous-infrastructure friendly (the host carries boxes, not content — storage is ciphertext).