Skip to main content

Deployment

How each component of the Qubital system is built, packaged, and deployed.

Component map

ComponentRuntimeRegistryDeploy trigger
Go backend (API server + eventsync worker)SevallaGHCR (ghcr.io/<org>/qubital-backend-prod)Manual (workflow_dispatch)
office-router WorkerCloudflare Workerswrangler deploy
upload-workerCloudflare Workerswrangler deploy
Recording pipeline (livekit-listener + video-upload-job)Google Cloud RunGCP Artifact RegistryManual (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

  1. Trigger build-push-prod.yaml via workflow_dispatch with a semver version string (e.g. 1.2.3)
  2. Workflow validates the version is strictly greater than all versions already published to GHCR (prevents downgrades)
  3. Tests run (full suite + race + smoke + lint) — build does not start if any step fails
  4. Docker image built for linux/amd64, scanned with Trivy (blocks on HIGH/CRITICAL)
  5. Image pushed to GHCR with three tags: <version>, sha-<git-sha>, and optionally latest
  6. SBOM and provenance attestations attached to the image in GHCR
  7. 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:

ComponentTypeEntry pointPurpose
livekit-listenerCloud Run Service (always-on)dist/service.jsReceives egress_ended webhooks from LiveKit, triggers the upload job
video-upload-jobCloud Run Job (on-demand)dist/job.jsFetches 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