Deployment
How each component of the Qubital system is built, packaged, and deployed.
Component map
| Component | Runtime | Registry | Deploy trigger |
|---|---|---|---|
| Go backend (API server + eventsync worker) | Sevalla | GHCR (ghcr.io/<org>/qubital-backend-prod) | Manual (workflow_dispatch) |
| office-router Worker | Cloudflare Workers | — | wrangler deploy |
| upload-worker | Cloudflare Workers | — | wrangler deploy |
| Recording pipeline (livekit-listener + video-upload-job) | Google Cloud Run | GCP Artifact Registry | Manual (workflow_dispatch) |
Go backend — Sevalla via GHCR
The backend is containerised and deployed on Sevalla. Sevalla pulls the Docker image from GHCR. The image is built and pushed by the build-push-prod.yaml workflow.
Release flow
- Trigger
build-push-prod.yamlviaworkflow_dispatchwith a semver version string (e.g.1.2.3) - Workflow validates the version is strictly greater than all versions already published to GHCR (prevents downgrades)
- Tests run (full suite + race + smoke + lint) — build does not start if any step fails
- Docker image built for
linux/amd64, scanned with Trivy (blocks on HIGH/CRITICAL) - Image pushed to GHCR with three tags:
<version>,sha-<git-sha>, and optionallylatest - SBOM and provenance attestations attached to the image in GHCR
- Sevalla pulls the new image on the next deploy (configured in Sevalla dashboard)
Image naming
ghcr.io/<org>/qubital-backend-prod:<version>
ghcr.io/<org>/qubital-backend-prod:sha-<git-sha>
ghcr.io/<org>/qubital-backend-prod:latest # only if tag_latest input is true
The org name is normalised to lowercase — GHCR rejects uppercase paths.
Cloudflare Workers
Both Cloudflare Workers (office-router and upload-worker) are deployed via the Wrangler CLI. There is no automated CI/CD pipeline for them — deployments are manual.
# From the qubital-workers repo root
pnpm deploy --env production # office-router or upload-worker
Each worker has staging and production environments defined in its wrangler.jsonc.
Recording pipeline — Google Cloud Run
The recording pipeline lives in qubital-workers/pkg/google-cloud-uploader/ and is deployed to Google Cloud Run via the build-publish.yaml workflow (workflow_dispatch).
One Docker image is built and deployed as two distinct components:
| Component | Type | Entry point | Purpose |
|---|---|---|---|
livekit-listener | Cloud Run Service (always-on) | dist/service.js | Receives egress_ended webhooks from LiveKit, triggers the upload job |
video-upload-job | Cloud Run Job (on-demand) | dist/job.js | Fetches recording from R2, uploads to YouTube, logs to Google Sheets, notifies Slack |
Both run under the video-sa GCP service account. The image is pushed to GCP Artifact Registry before deployment.
Recording pipeline flow
LiveKit Cloud
│ egress_ended webhook
▼
livekit-listener (Cloud Run Service)
│ triggers
▼
video-upload-job (Cloud Run Job)
│ fetches file from Cloudflare R2 (temporary recording storage)
│ uploads to YouTube as unlisted video
│ logs entry to Google Sheets
│ sends link to Slack