Automated VOD upload from Twitch/Kick to YouTube after streaming.
- Node.js 24+
- PostgreSQL 14+
- Redis 6+
- PgBouncer
- FlareSolverr (for Kick)
- TimescaleDB
This project uses two databases:
Stores tenant configurations and admin users.
# Create the database
createdb archiveThe tenants and admins tables must be created manually before use.
Each tenant has its own database for VODs, chat logs, emotes, etc. The database URL is stored in the tenant's record in the meta database.
Migrations are applied automatically when creating a new tenant via the create-tenant script. For migrating legacy databases, use:
NODE_ENV=development npx tsx scripts/migrate-streamer.ts-
Install dependencies
cd path/to/archive npm install -
Set up environment variables
cp .env.example .env.development # Edit .env.development with your configuration -
Create a tenant
NODE_ENV=development npx tsx scripts/create-tenant.ts
-
Start the application
NODE_ENV=development npm run dev
FlareSolverr is required for Kick stream detection and VOD fetching. Run it as a Docker container:
docker run -d \
--name=flaresolverr \
-p 8191:8191 \
-e LOG_LEVEL=info \
--restart unless-stopped \
ghcr.io/flaresolverr/flaresolverr:latestOr install standalone:
# Linux
sudo apt install -y chromium-browser xvfb
pip install flaresolverr
flaresolverr --port 8191
# Windows
# Download from https://github.com/FlareSolverr/FlareSolverr/releases
# Run: flaresolverr.exe --port 8191Verify it's running:
curl -X POST 'http://localhost:8191/v1' \
-H 'Content-Type: application/json' \
-d '{"cmd":"status"}'PgBouncer is required as the connection pooler between the application and PostgreSQL. It listens on 127.0.0.1:6432 in transaction pool mode.
Installation:
# Debian/Ubuntu
sudo apt install pgbouncer
# Docker
docker run -d \
--name=pgbouncer \
-p 6432:6432 \
-v /path/to/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini \
-v /path/to/userlist.txt:/etc/pgbouncer/userlist.txt \
--restart unless-stopped \
postgresConfiguration:
pgbouncer.ini— seepgbouncer.iniin repo root for a reference configuserlist.txt— PgBouncer auth file (format:"username"="md5hash"or"username"="password"depending onauth_type)listen_addr = 127.0.0.1— never expose PgBouncer externallypool_mode = transaction— connections released after each transaction- Wildcard
* = host=<PG_HOST> port=5432in[databases]for dynamic tenant DBs
Set PGBOUNCER_URL in your environment variables (e.g., postgresql://archive@localhost:6432/archive).
TimescaleDB is used as the hypertable engine for the chat_messages table, providing time-based partitioning and automatic compression for chat log data.
- PostgreSQL 14+ with the TimescaleDB extension installed
- The extension is created automatically by the migration scripts
Ubuntu/Debian:
sudo apt install postgresql-timescaledb
sudo systemctl restart postgresqlDocker:
docker run -d \
--name=timescaledb \
-p 5432:5432 \
-e POSTGRES_PASSWORD=yourpassword \
-v /path/to/data:/var/lib/postgresql/data \
--restart unless-stopped \
timescaledb/timescaledb:latest-pg16The chat_messages table is converted to a hypertable with the following configuration:
| Setting | Value | Purpose |
|---|---|---|
| Partition column | created_at |
Chunks created by time intervals |
| Chunk interval | 7 days | Each chunk covers a 7-day window |
| Compression | Enabled | Reduces storage for older chat data |
| Compression segment | vod_id |
Keeps messages from the same VOD together |
| Compression order | content_offset_seconds ASC |
Optimizes for sequential replay queries |
| Auto-compress policy | 30 days | Chunks older than 30 days are compressed automatically |
The chat_messages table uses a composite primary key on (id, created_at). This is a TimescaleDB requirement: any unique index on a hypertable must include the partitioning column. Without it, TimescaleDB would need to scan every chunk globally to enforce uniqueness, destroying write performance. Including created_at guarantees uniqueness checks are confined to the active chunk.
Check that the hypertable is set up correctly:
SELECT hypertable_name, num_chunks, compression_enabled FROM timescaledb_information.hypertables;
SELECT * FROM timescaledb_information.compression_settings WHERE hypertable_name = 'chat_messages';
SELECT job_id, application_name, schedule_interval, max_runtime, scheduled FROM timescaledb_information.jobs;Monitor chunk status:
SELECT chunk_name, range_start, range_end, is_compressed FROM timescaledb_information.chunks
WHERE hypertable_name = 'chat_messages' ORDER BY range_start DESC;Manually compress a chunk:
SELECT compress_chunk(chunk_name::regclass, if_not_compressed => true)
FROM timescaledb_information.chunks
WHERE hypertable_name = 'chat_messages' AND NOT is_compressed;Decompress a chunk (rarely needed):
SELECT decompress_chunk(chunk_name::regclass)
FROM timescaledb_information.chunks
WHERE hypertable_name = 'chat_messages' AND is_compressed;Monitor compression ratio:
SELECT
ch.chunk_name,
ch.is_compressed,
pg_size_pretty(pg_total_relation_size(pc.oid)) AS total_size
FROM timescaledb_information.chunks ch
JOIN pg_class pc ON pc.relname = ch.chunk_name
JOIN pg_namespace ns ON ns.oid = pc.relnamespace
WHERE ch.hypertable_name = 'chat_messages'
ORDER BY ch.range_start DESC;- No configuration changes needed — TimescaleDB is transparent to the application. All queries use standard SQL and Kysely queries work identically.
- PgBouncer compatibility — TimescaleDB uses standard PostgreSQL protocol, so PgBouncer works without any special configuration.
- Compression is transparent — The application reads and writes compressed chunks automatically. Decompression happens on-the-fly during queries.
- Chunk retention — Chunks are never automatically dropped. If you need to delete old data, use
drop_chunks('chat_messages', older_than => INTERVAL '90 days').
See .env.example for all available configuration options. Key variables:
PGBOUNCER_URL: PgBouncer connection string (required, e.g.,postgresql://archive@localhost:6432/archive)META_DATABASE_URL: Connection string for the meta databaseREDIS_URL: Redis connection for caching and job queuesENCRYPTION_MASTER_KEY: 64-character hex string for encrypting sensitive dataJWT_SECRET: Secret for admin dashboard authenticationFLARESOLVERR_BASE_URL: FlareSolverr API endpoint (default:http://localhost:8191)TWITCH_CLIENT_ID: Twitch app client ID (shared across all tenants for Helix API)TWITCH_CLIENT_SECRET: Twitch app client secret (shared across all tenants for Helix API)TMP_PATH: Local SSD/NVMe path for intermediate file operations (optional, enables storage optimization)VOD_PATH: Final VOD storage pathLIVE_PATH: Final Live storage path for twitch-recorder-go
Ensure .env exists and NODE_ENV is set before running scripts.
To upload 15+ minute videos, verify your YouTube channel: https://www.youtube.com/verify
To make videos public automatically, you need to complete Google's API audit. See: https://developers.google.com/youtube/v3/docs/videos/insert