Media processing pipeline on Railway with Ittybit
Instead of polling for task completion, let Ittybit push results to you. Deploy a small webhook receiver on Railway, submit media for processing, and get async results posted back to your endpoint when each task finishes.
Project setup
Scaffold a new project with an Express server and a Postgres client:
mkdir ittybit-pipeline && cd ittybit-pipeline
npm init -y
npm install express pg
Set your environment variables in the Railway dashboard or via railway variables set:
ITTYBIT_API_KEY=your_ittybit_api_key
DATABASE_URL=your_railway_postgres_url
RAILWAY_PUBLIC_DOMAIN=your-app.up.railway.app
Create the database table
Create a media_jobs table to track submissions and results:
CREATE TABLE media_jobs (
id SERIAL PRIMARY KEY,
task_id TEXT UNIQUE,
input_url TEXT NOT NULL,
status TEXT DEFAULT 'pending',
output_url TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
completed_at TIMESTAMPTZ
);
Build the server
The server has two routes: one to accept media URLs and dispatch tasks, and one to receive webhook callbacks from Ittybit.
import express from "express";
import pg from "pg";
const app = express();
app.use(express.json());
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const ITTYBIT_API_KEY = process.env.ITTYBIT_API_KEY;
const RAILWAY_DOMAIN = process.env.RAILWAY_PUBLIC_DOMAIN;
// Accept a media URL, dispatch an Ittybit task with a webhook
app.post("/process", async (req, res) => {
const { url, kind = "video", options = {} } = req.body;
const task = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: url,
kind,
options,
webhook_url: `https://${RAILWAY_DOMAIN}/webhooks/ittybit`,
}),
});
const data = await task.json();
await pool.query(
"INSERT INTO media_jobs (task_id, input_url, status) VALUES ($1, $2, $3)",
[data.id, url, "processing"]
);
res.json({ task_id: data.id, status: "processing" });
});
// Receive completion webhooks from Ittybit
app.post("/webhooks/ittybit", async (req, res) => {
const { id, status, output } = req.body;
await pool.query(
`UPDATE media_jobs
SET status = $1, output_url = $2, completed_at = now()
WHERE task_id = $3`,
[status, output?.url ?? null, id]
);
res.sendStatus(200);
});
// Check job status
app.get("/jobs/:taskId", async (req, res) => {
const { rows } = await pool.query(
"SELECT \* FROM media_jobs WHERE task_id = $1",
[req.params.taskId]
);
if (!rows.length) return res.status(404).json({ error: "not found" });
res.json(rows[0]);
});
app.listen(process.env.PORT || 3000);
# Submit a video for processing
curl -X POST https://your-app.up.railway.app/process \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/uploads/video.mov",
"kind": "video",
"options": { "width": 1280, "format": "mp4", "quality": "high" }
}'
# Check status
curl https://your-app.up.railway.app/jobs/task_abc123 Deploy to Railway
Connect your repo to Railway via GitHub or push directly:
railway up
Railway auto-detects the Node.js project, installs dependencies, and starts the server. Add a Postgres plugin from the Railway dashboard and the DATABASE_URL is injected automatically.
What happens at runtime
- You POST a media URL to
/process - Your server creates an Ittybit task with
webhook_urlpointing to your Railway domain - Ittybit processes the media asynchronously
- On completion, Ittybit POSTs the result to
/webhooks/ittybit - Your server updates Postgres with the output URL and status
Multiple tasks per upload
Submit several tasks for the same input to build a full pipeline:
const tasks = [
{ kind: "video", options: { width: 1920, format: "mp4", quality: "high" } },
{ kind: "image", options: { width: 640, format: "webp", start: 2 } },
{ kind: "video", options: { width: 480, format: "mp4", quality: "low" } },
];
const results = await Promise.all(
tasks.map((task) =>
fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: "https://example.com/uploads/video.mov",
...task,
webhook_url: `https://${RAILWAY_DOMAIN}/webhooks/ittybit`,
}),
}).then((r) => r.json())
)
);
# HD version
curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/uploads/video.mov",
"kind": "video",
"options": { "width": 1920, "format": "mp4", "quality": "high" },
"webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
}'
# Thumbnail
curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/uploads/video.mov",
"kind": "image",
"options": { "width": 640, "format": "webp", "start": 2 },
"webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
}'
# Low-res preview
curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/uploads/video.mov",
"kind": "video",
"options": { "width": 480, "format": "mp4", "quality": "low" },
"webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
}' Each task completes independently and fires its own webhook. Your /webhooks/ittybit handler updates the database as each result arrives.