Static hosting
A bucket and a CDN. Planned, not shipped — there is no static-publish command yet. No rollouts, no targeting, no recall even once it lands.
The engine and SDK are open source, and the SDK will talk to any base URL — so you can serve patches from infrastructure you control.
This page is deliberately honest about what that gets you and what it doesn’t.
Static hosting
A bucket and a CDN. Planned, not shipped — there is no static-publish command yet. No rollouts, no targeting, no recall even once it lands.
Reference server
A small service that decides per request, so percentage rollouts, cohorts and rollback recall all work. You run it and you support it.
Patch Cloud
The hosted control plane — rollouts, targeting, analytics, audit, team accounts, and someone to call.
Most teams should use the hosted service. The self-host path exists so that choosing it isn’t a one-way door.
Static publishing is not shipped yet (see the “Static only” tab). This is what the two paths will look like when it is, so you can plan — and because the limits are structural, not temporary:
| Capability | Static | Server |
|---|---|---|
| Serve the newest patch | Yes | Yes |
| Fingerprint gating | Yes | Yes |
| Rollback by republishing | Yes | Yes |
| Percentage rollouts | No | Yes |
| Cohort / version targeting | No | Yes |
| Rollback recall — pull a patch back from devices that already took it | No | Yes |
| Device-cap enforcement, adoption metrics | No | Yes |
Rollout bucketing is computed per device, per request. A file in a bucket can’t do that. Recall is worse: it needs to know what each device is currently running, which is a query, not an object.
Wherever you host, this is the only client change:
Patch.configure(.init( appKey: "pak_…", apiBaseURL: URL(string: "https://patches.example.com/api/v1")))And the CLI:
patchcli release --base-url https://patches.example.com# or: export PATCH_API_URL=https://patches.example.comSetting apiBaseURL to nil disables remote checks entirely — the app runs
whatever module is bundled or already cached.
The shape is the same everywhere: object storage for the module bytes, a CDN in front of it, a small stateless service for the check endpoint, and Postgres for releases and device state. Nothing here is exotic — it fits in the smallest tier of any of these providers.
patchcli ──HTTPS──▶ Cloud Run ──▶ Cloud SQL (Postgres) │ releases · fingerprints · check-ins └──▶ GCS bucket (module.wasm.br) │ device ◀── Cloud CDN ◀───────┘Bucket for modules.
gcloud storage buckets create gs://patch-modules-prod \ --location=us-central1 --uniform-bucket-level-accessPostgres. The smallest shared-core tier is plenty — this database holds releases and device check-ins, not user data.
gcloud sql instances create patch-db \ --database-version=POSTGRES_16 --tier=db-f1-micro --region=us-central1Deploy the check service to Cloud Run, pointed at both.
gcloud run deploy patch-api \ --image=<your-image> --region=us-central1 --allow-unauthenticated \ --set-env-vars=DATABASE_URL=...,STORAGE_BUCKET=patch-modules-prodPut Cloud CDN in front of the bucket via a backend bucket on an external HTTPS load balancer. Modules are immutable and content-addressed, so cache them hard.
patchcli ──HTTPS──▶ ALB ──▶ ECS Fargate ──▶ RDS Postgres │ └──▶ S3 bucket (module.wasm.br) │ device ◀── CloudFront ◀──────┘Bucket for modules, with public access blocked — CloudFront reads it through an Origin Access Control and nothing else does.
aws s3api create-bucket --bucket patch-modules-prod --region us-east-1aws s3api put-public-access-block --bucket patch-modules-prod \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=truePostgres.
aws rds create-db-instance --db-instance-identifier patch-db \ --engine postgres --db-instance-class db.t4g.micro --allocated-storage 20Run the check service on ECS Fargate behind an ALB. App Runner works too and is less to configure; Fargate is the right answer if you already have a VPC and want the service inside it.
CloudFront distribution over the bucket, using the OAC from step 1.
patchcli ──HTTPS──▶ Container Apps ──▶ Azure Database for PostgreSQL │ └──▶ Blob Storage (module.wasm.br) │ device ◀── Azure Front Door ◀──┘Storage account and container for the modules.
az storage account create --name patchmodules --sku Standard_LRS \ --resource-group patch-rg --location eastusaz storage container create --name modules --account-name patchmodulesPostgres.
az postgres flexible-server create --name patch-db \ --resource-group patch-rg --tier Burstable --sku-name Standard_B1msDeploy the check service to Container Apps.
az containerapp create --name patch-api --resource-group patch-rg \ --image <your-image> --ingress external --target-port 8000 \ --env-vars DATABASE_URL=... STORAGE_CONTAINER=modulesAzure Front Door in front of the blob container for edge caching.
The idea: emit the module plus the JSON manifest the SDK expects, upload both to any bucket or static host, and point the SDK at it.
Patch.configure(.init( appKey: "pak_…", apiBaseURL: URL(string: "https://cdn.example.com/patch")))Rolling back would mean republishing the previous manifest.
If you need self-hosting today, use the server path in the other tabs — the
SDK’s apiBaseURL already points anywhere, so the constraint is that something
must answer the check endpoint, not that it must be ours.
The check endpoint is a small read — one row lookup plus a hash. It is not the expensive part of your infrastructure.
| Fleet | Check service | Postgres | Egress |
|---|---|---|---|
| Up to ~50k devices | One small instance | Smallest tier | Patches are KB, not MB |
| ~500k devices | Two instances behind a load balancer | Smallest tier plus a read replica | CDN absorbs it |
| Millions | Autoscale on CPU | Managed, connection-pooled | CDN absorbs it |
Modules are content-addressed and immutable, so CDN hit rates are very high — the origin only serves the first request per release per edge.
Modules are immutable. A release is content-addressed by its SHA-256. Never overwrite one in place — publish a new version and update the manifest. The SDK verifies the hash before activating, so a swapped or corrupted object fails closed rather than running.
Keep the fingerprint contract. The device reports the fingerprint of the build it’s running, and your service must only serve modules built against that same shell. Getting this wrong is the one way to ship a patch that genuinely breaks an app — see the compatibility fingerprint.
Serve the compressed artifact as-is. Modules ship brotli-compressed
(.wasm.br). Don’t set Content-Encoding: br on the object — the SDK fetches the
compressed bytes and decompresses them itself, so a transport-level re-encode
breaks the hash check.
Retention. Device check-ins accumulate a row per device per app. Add a retention job; nothing prunes it for you.
patchcli init browser onboarding — you hand-write .Patch.yml and issue
your own app keys