Skip to content

Architecture overview

BCDock is a three-tier system. Knowing where the boundaries fall makes the rest of these pages - and a fair bit of the CLI behaviour - easier to reason about.

The three tiers

┌────────────────────────────────────────────────────────────────┐
│  Clients                                                        │
│                                                                  │
│   Portal (Next.js)        bcdock CLI       Third-party / agents │
│        │                       │                       │         │
└────────┼───────────────────────┼───────────────────────┼─────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌────────────────────────────────────────────────────────────────┐
│  Platform API  (C# / ASP.NET Core)                             │
│                                                                  │
│  Multi-tenancy │ Billing │ Quotas │ Auth │ DB state │ API surface│
└──────────────────────────┬─────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│  bcdock-infra  (Go)                                             │
│                                                                  │
│  VMs │ Images │ OS setup │ Containers │ SSH │ Secrets (Key Vault)│
└──────────────────────────┬─────────────────────────────────────┘
                  Azure (compute, storage, DNS, Key Vault)

Platform API (C#)

ASP.NET Core 10. Owns:

  • Multi-tenancy - companies, memberships, company-scoped query isolation
  • Billing - subscriptions, usage records, dual-rate metering
  • Quotas - per-tier env counts, trial caps, suspension
  • Auth - OTP exchange, JWTs, API keys with scopes
  • DB state - managed relational database; background-job processing for long-running work
  • API surface - what the CLI, portal, and integrations call

The Platform never touches Azure directly. Anything that requires creating or destroying infrastructure routes through bcdock-infra.

bcdock-infra (Go)

Owns the how:

  • VM provisioning and lifecycle (pools)
  • Image builds - generic + versioned, the 2-stage system
  • OS setup on pool VMs - Windows + Docker + BcContainerHelper
  • Container lifecycle on the pool VM (via the pool agent)
  • SSH operations for setup and debugging
  • Secret management - fetching from Key Vault, never logging

The Platform calls bcdock-infra over an HTTP API; bcdock-infra streams progress back as NDJSON. The platform side owns the why (which env to create for which company); the infra side owns the how (the Docker container lifecycle and Tofu-managed VMs).

Clients

Three first-class clients of the Platform API, all consuming the same surface:

  • Portal (app.bcdock.io) - Next.js + React web application
  • CLI (bcdock) - single static Go binary
  • Third-party / agents - anything that can speak HTTPS + JSON

There is no second, privileged API. Staff use the same Platform API as customers, with appropriately scoped tokens.

Service boundaries

The boundary between the Platform (C#) and bcdock-infra (Go) is load-bearing for the architecture. It enforces three things:

  1. Cloud agnosticism in the Platform. The C# layer doesn't import Azure SDK packages. If we ever needed to support a non-Azure substrate, only bcdock-infra would change.
  2. Auditability. Provisioning operations stream structured progress back as NDJSON; every line gets stamped with the env or pool ID and persisted into the platform's audit trail.
  3. Secret containment. Azure storage credentials and Key Vault tokens never leave bcdock-infra. The Platform requests "create a SAS for blob X" or "fetch secret Y"; bcdock-infra returns the result.

Pool layer

A pool is an Azure VM hosting 2-9 BC environments. We never stop pools - they always have a public FQDN, Traefik fronting them, and capacity for new environments. The autoscaler creates pools when capacity is tight and tears them down when they're idle.

Each pool runs:

  • Traefik v3 - reverse proxy, terminates TLS, routes to the right BC container per request
  • Pool agent - Windows executable that talks to the Platform over Traefik on /pool-agent. Manages container lifecycle.
  • N BC containers - one per environment, each with its own database, admin password, and DNS subdomain

Detail lives in images and pools and URL shape.

Storage layer

Three storage classes:

  • Managed relational database (Azure-hosted) - all platform state: users, companies, environments, usage records, audit log, provisioning logs. Single instance in australiaeast (Sydney).
  • Azure Blob Storage (per region) - hibernation backup snapshots, data export ZIPs. 7-day soft-delete on hibernation backups.
  • Azure Key Vault (core + per region) - TLS cert in core; per-env passwords and pool SSH keys in regional KVs.

Why per-region storage: hibernation backups are large and region-bound (you don't want to hibernate from US-West-2 and resume from australiaeast). Data export blobs are short-lived (24h SAS).

Cross-cutting

  • Provisioning logs - every long-running operation streams structured progress (type: log, type: result, type: error over NDJSON). The Platform persists every line into a per-env / per-pool audit trail that customers can stream via bcdock env logs.
  • OpenTelemetry - exported to Azure Monitor (Application Insights). One trace per request, spans across the C#/Go boundary via baggage.
  • DNS - Azure DNS zone for the production hostname (bcdock.io). Per-pool A records, per-env CNAME records under *.bcdock.io. Wildcard TLS via DNS-01.

Where to read more

  • Images and pools - 2-stage image system + pool model + autoscaler
  • URL shape - per-env subdomains, wildcard TLS, agent and dev endpoints
  • Hibernation - active vs stored states, the dual-rate billing model
  • Anonymisation - anonymise-in-place for GDPR / APP, why and how