One honest
update check.
Updater is an append-only release ledger with a small HTTP API. Publish versions, ask what's latest, and show your own update button. No forced migrations, no silent edits.
https://upd.vps.disgusting.work/api/v1Overview
Each project has a stable slug, one scoped API key, and an append-only list of releases. A release belongs to a channel (stable, beta, nightly) and a platform (macOS, Windows, Linux, iOS, Android, web, or universal).
Authentication
Create a project in the dashboard and save its API key. Send it as a Bearer token. Keys are scoped to one project and are stored hashed — rotate at any time.
Authorization: Bearer upd_live_xxxxxxxxxxxxxxxxxxxxxxxx
# or
X-API-Key: upd_live_xxxxxxxxxxxxxxxxxxxxxxxxPublic vs private. Public projects allow unauthenticated GET checks. Private projects require a valid project key. POST always requires a key.
Quick start
Publish a release from CI, then have your app ask whether it should update.
export UPDATER_API_KEY="upd_live_..."
curl -X POST "https://upd.vps.disgusting.work/api/v1/projects/your-project/releases" \
-H "Authorization: Bearer $UPDATER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"version": "2.4.0",
"channel": "stable",
"platform": "macos",
"title": "Glass panels & faster sync",
"notes": "A calmer canvas, quicker sync, and a fix for offline drafts.",
"downloadUrl": "https://cdn.example.com/Canvas-2.4.0.dmg",
"checksum": "8f14e45fceea167a5a36dedd4bea2543...",
"sizeBytes": 48234496
}'// 201 Created
{
"release": {
"version": "2.4.0",
"channel": "stable",
"platform": "macos",
"immutable": true
},
"checkUrl": "https://upd.vps.disgusting.work/api/v1/projects/your-project/update"
}Check for updates
/projects/{slug}/updateThe one endpoint your app needs. Pass its current version, platform, and optional channel.
currentstringSemantic version the client is running, e.g. 2.3.1.platformstringOptional. Defaults to the highest release across platforms. Pass a platform (or universal) to scope it.channelstringstable by default. Also beta, nightly.curl "https://upd.vps.disgusting.work/api/v1/projects/your-project/update?current=2.3.1&platform=macos&channel=stable" \
-H "Authorization: Bearer $UPDATER_API_KEY"{
"project": { "slug": "your-project", "name": "Canvas" },
"currentVersion": "2.3.1",
"channel": "stable",
"platform": "macos",
"updateAvailable": true,
"reason": "update_available",
"latest": {
"version": "2.4.0",
"channel": "stable",
"platform": "macos",
"downloadUrl": "https://cdn.example.com/Canvas-2.4.0.dmg",
"checksum": "sha256:8f14e45f...",
"sizeBytes": 48234496,
"notes": "A calmer canvas, quicker sync…",
"metadata": {},
"publishedAt": "2026-05-18T09:41:00.000Z",
"immutable": true
},
"checkedAt": "2026-05-18T09:42:10.000Z"
}Comparison is semantic. 2.4.0-beta.2 is older than 2.4.0, and 2.10.0 is newer than 2.9.9. If no newer release exists, updateAvailable is false.
Latest release
/projects/{slug}/latestReturns the highest semantic version for a channel and platform, with no version comparison. Useful for public download pages.
curl "https://upd.vps.disgusting.work/api/v1/projects/your-project/latest?platform=windows&channel=beta"Release history
/projects/{slug}/releasesLists releases newest-first. Supports optional channel, platform, and limit (1–100). Private project keys are checked.
{
"project": { "slug": "your-project", "name": "Canvas" },
"count": 2,
"releases": [
{ "version": "2.4.0", "channel": "stable", "platform": "macos", "immutable": true },
{ "version": "2.3.1", "channel": "stable", "platform": "macos", "immutable": true }
]
}Publish a release
/projects/{slug}/releasesCreates a new immutable release. This endpoint requires a project API key. Duplicate versions on the same channel and platform return 409.
versionyesSemantic version. Build metadata is ignored for ordering.downloadUrlyesAbsolute URL to the artifact.channelnoDefault stable.platformnoDefault universal.titlenoShort headline for the release.notesnoRelease notes, preserved as plain text.checksumnoSHA-256 hex, with or without a sha256: prefix.sizeBytesnoPositive integer.minVersionnoMinimum source version for this update.metadatanoJSON object stored verbatim, e.g. architecture or rollout.{
"version": "2.4.1",
"channel": "beta",
"platform": "windows",
"downloadUrl": "https://cdn.example.com/Canvas-2.4.1.exe",
"checksum": "sha256:9c56cc51...",
"sizeBytes": 52100312,
"metadata": { "architecture": "x64", "mandatory": false }
}Deploy source code
For running web apps, Updater can trigger a real source deployment — not just return a download URL. Configure an HTTP deploy hook or a server-side command in the project's Deploy tab, then trigger it from the console or another app's admin UI.
/projects/{slug}/deployRequires the project API key. Both fields are optional: omit them to deploy the latest stable release, pass releaseId to target one release, or pass version to target a version.
curl -X POST "https://upd.vps.disgusting.work/api/v1/projects/your-web-app/deploy" \
-H "Authorization: Bearer $UPDATER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version":"2.4.1"}'{
"deployment": {
"id": "dep_…",
"version": "2.4.1",
"trigger": "api",
"kind": "command",
"status": "success",
"duration_ms": 38420,
"output": "Already up to date.
Build complete.",
"error": null
}
}Two deployment targets. An HTTP hook is safest and works with Coolify, Dokploy, Vercel, Netlify, GitHub Actions, or your own deploy endpoint. A server command runs on the Updater host as a shell command, so only admins can configure it.
Deploy hooks receive x-updater-signature: t=…,v1=…, an HMAC-SHA256 of timestamp + "." + rawBody using the deploy signing secret shown in the project.
Update from another web UI
Put the API key on the server side of the other project, never in browser JavaScript. Its admin web UI can call a small route that invokes Updater and relays the deployment status and output.
// your-web-app/server/admin-update.js
app.post("/admin/update", requireAdmin, async (req, res) => {
const response = await fetch(
"https://upd.vps.disgusting.work/api/v1/projects/your-web-app/deploy",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UPDATER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ version: req.body.version }),
}
);
const result = await response.json();
res.status(response.status).json(result);
});Deployment history is available at GET /projects/{slug}/deployments. Every deployment is recorded with its trigger, status, duration, actor, and captured output. A missing deployment target returns 409; a failed target returns 502 with the captured output.
Command deployments run with Updater's system privileges. Only grant workspace access to people you trust, protect project API keys, and prefer deploy hooks when your hosting platform supports them.
Webhooks
Add a webhook URL in the project's Webhooks tab. Every publish dispatches release.published with an HMAC-SHA256 signature.
POST https://api.yourapp.com/updates/webhook
X-Updater-Signature: t=1760000000,v1=hex_hmac_sha256
Content-Type: application/json
{
"event": "release.published",
"project": { "slug": "your-project", "name": "Canvas" },
"release": { "version": "2.4.0", "downloadUrl": "…" }
}import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyUpdaterWebhook(rawBody, header, secret) {
const [timestampPart, signaturePart] = header.split(",");
const timestamp = timestampPart.split("=")[1];
const signature = signaturePart.split("=")[1];
const expected = createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Errors & limits
Errors use a consistent JSON shape. Update checks are rate limited to 240 requests per minute per IP.
{
"error": "This project is private. Provide a project API key.",
"code": "private_project"
}Invalid payload, version, platform, or channel.
Missing key, or the project is private.
The key belongs to another project.
Project or release not found.
That immutable version already exists.
The project is archived.
Too many checks. Back off and retry.
Immutability, in practice
A release row cannot be updated or deleted — at the database level. To fix a bad build, publish a new version. To roll back, publish the older code under a new version. History stays honest, and every client can trust that a version means one thing forever.
Ready to wire it up?
Create a project, grab a key, and make your first request.