Skip to content

Testing with Desk

Desk can spin up a local Kubernetes cluster with skew protection pre-configured. This is the easiest way to try out versioned deployments before going to production.

Desk ships two profiles that enable skew protection — they are alternatives, pick whichever fits:

ProfileRuns ICC / Machinist / Workflow fromUse when
ossReleased container imagesYou just want to try skew protection — no source checkout or build required.
skew-protectionLocal repositories with hot reloadYou are developing or debugging ICC itself and want your local changes running live.

Both profiles install Envoy Gateway as the Gateway API controller, create a Gateway resource, and enable skew protection. They use different testing timeouts, shown below. The rest of this guide works with either profile; substitute --profile oss or --profile skew-protection in the commands.

Follow the Getting Started guide to install Desk and configure your environment (GitHub OAuth, .env file, /etc/hosts).

Then set up one of the two profiles below.

Option A — oss profile (released images)

Section titled “Option A — oss profile (released images)”

No source checkout is needed. Desk pulls the released ICC, Machinist, and Workflow images, so this is the fastest way to get a working cluster. Start it with:

Terminal window
desk cluster up --profile oss

Option B — skew-protection profile (local hot reload)

Section titled “Option B — skew-protection profile (local hot reload)”

This profile runs ICC, Machinist, and Workflow with hot reload from local repositories (it extends Desk’s development profile). Clone the three repositories (all public) and point the matching Desk .env variable at each checkout:

VariableClone URL
ICC_REPOhttps://github.com/platformatic/intelligent-command-center.git
MACHINIST_REPOhttps://github.com/platformatic/machinist.git
WORKFLOW_REPOhttps://github.com/platformatic/platformatic-world.git

WORKFLOW_REPO must point to the platformatic-world monorepo root, not packages/workflow: Desk mounts the repo at /app and runs the workflow service from /app/packages/workflow.

Hot reload runs the local repos directly, and some ICC services (e.g. cluster-manager) require() compiled .js files that only exist as TypeScript in a fresh checkout. Build the ICC repo once before starting the cluster:

Terminal window
cd "$ICC_REPO"
npm run build:dev

Then start the cluster:

Terminal window
desk cluster up --profile skew-protection

Either profile creates a local k3d cluster with all the necessary components. Verify that the Gateway is programmed:

Terminal window
kubectl get gateway platformatic -n platformatic

The PROGRAMMED column must be True.

Both profiles enable skew protection, but skew-protection leaves more time for hot-reload development and manual inspection:

Settingossskew-protectionProduction Default
skew_protection.enabletruetruefalse
skew_protection.http_grace_period_ms30000 (30 sec)120000 (2 min)1800000 (30 min)
skew_protection.http_max_alive_ms180000 (3 min)900000 (15 min)86400000 (24h)
skew_protection.workflow_grace_period_ms300000 (5 min)300000 (5 min)3600000 (1h)
skew_protection.workflow_max_alive_ms900000 (15 min)900000 (15 min)259200000 (72h)
skew_protection.check_interval_ms10000 (10 sec)10000 (10 sec)60000 (1 min)
skew_protection.traffic_window_ms30000 (30 sec)60000 (1 min)1800000 (30 min)
skew_protection.cookie_max_age43200 (12h)43200 (12h)43200 (12h)
skew_protection.auto_cleanupfalsefalsefalse

Use the --version flag to deploy a versioned application. Desk automatically sets the correct app.kubernetes.io/name and plt.dev/version labels, and passes the same value to the image build as PLT_DEPLOYMENT_ID so the assets carry a matching ?dpl=:

Terminal window
desk deploy --dir ./my-app --version v1 --profile skew-protection

Wait for the pod to be ready:

Terminal window
kubectl get pods -n platformatic -l app.kubernetes.io/name=my-app

Verify that ICC created the HTTPRoute:

Terminal window
kubectl get httproute -n platformatic

Open your browser and navigate to https://svcs.gw.plt/my-app/. The skew-protection profile runs default_routing_mode: query, so there is no cookie to look for. Use the developer tools (Network tab) and check that the asset URLs carry ?dpl=v1 — those are baked into the build, and they are what pins this page to version 1.

Terminal window
desk deploy --dir ./my-app --version v2 --profile skew-protection

Wait for both pods to be running:

Terminal window
kubectl get pods -n platformatic -l app.kubernetes.io/name=my-app

After ICC detects the new version, open the ICC dashboard at https://icc.plt/ and navigate to the Watt detail page for your application. The Deployments panel shows the version lifecycle in real time — the new version as Active and the previous version as Draining:

Deployments panel showing Active and Draining versions

A page already open — its assets keep requesting ?dpl=v1, so those requests continue to reach version 1 for as long as the page lives. You can confirm this in the Network tab. Note that this is per page load, not per browser: unlike cookie mode, a full reload is deliberately not sticky and re-pins to whatever is active.

A fresh navigation — reload the page, or open a new tab. It carries no ?dpl, so it is served by the active version, and the HTML it returns stamps every asset with ?dpl=v2. No incognito window is needed: with nothing stored client-side, an ordinary reload is already a new session.

Draining HTTP versions are expired after the grace period when ICC observes two consecutive zero-traffic checks, or when the max-alive ceiling is reached. With the skew-protection profile used in the examples, the HTTP grace period is 2 minutes and max alive is 15 minutes. The oss profile uses 30 seconds and 3 minutes respectively.

For demos and testing, you can also expire a version immediately by clicking the Expire button next to the draining version in the ICC dashboard. This skips the grace period and triggers the cleanup right away — the HTTPRoute rules for that version are removed and the Deployment is scaled to 0 replicas.

After expiration, the dashboard shows the version as Expired:

Deployments panel showing Active and Expired versions

Once expired, a request still carrying ?dpl=v1 matches no rule and falls through to version 2 rather than erroring. A page left open across the expiry may 404 on individual assets, since version 2 does not have version 1’s content-hashed filenames; reloading fixes it.

By default, Desk deploys apps under a path prefix on a shared hostname (https://svcs.gw.plt/<app-name>/). This works for most apps, but some frameworks — notably Next.js — make root-relative client-side fetch calls (e.g., fetch('/api/generate')) that break when the app is served under a sub-path.

The --hostname flag gives the app its own dedicated hostname with a root path prefix, matching how platforms like Vercel deploy apps:

Terminal window
desk deploy --dir ./my-app --version v1 --hostname my-app.plt --profile skew-protection

This tells ICC to create an HTTPRoute with hostnames: ["my-app.plt"] and a / path prefix instead of hostnames: ["svcs.gw.plt"] with /<app-name>.

ScenarioFlag
App works fine under a sub-pathNo flag needed (default path-prefix routing)
App makes root-relative API calls (e.g., Next.js)--hostname my-app.plt
App expects to own its entire domain--hostname my-app.plt

Add the hostname to your /etc/hosts file so it resolves to the local cluster:

Terminal window
echo "127.0.0.1 my-app.plt" | sudo tee -a /etc/hosts
Terminal window
# Deploy v1
desk deploy --dir ./birthday-card-generator \
--profile skew-protection \
--version v1 \
--hostname birthday-card-generator.plt
# Deploy v2
desk deploy --dir ./birthday-card-generator \
--profile skew-protection \
--version v2 \
--hostname birthday-card-generator.plt
# App is available at https://birthday-card-generator.plt/
# API routes like /api/generate work correctly

Skew protection works the same way — a fresh navigation is served by v2 and pinned to it by the ?dpl its assets carry, while pages already open on v1 keep reaching v1 until the version is expired.

The skew-protection profile also enables the Workflow Service, so you can test versioned workflow deployments alongside HTTP skew protection.

Workflow apps need the WORKFLOW_TARGET_WORLD environment variable in their Dockerfile. Desk auto-detects this and sets the plt.dev/workflow: "true" label automatically:

Terminal window
desk deploy --dir ./my-workflow-app \
--version v1 \
--hostname my-workflow-app.plt \
--profile skew-protection

Add the hostname to /etc/hosts:

Terminal window
echo "127.0.0.1 my-workflow-app.plt" | sudo tee -a /etc/hosts

Open https://my-workflow-app.plt/ and trigger a workflow through your app’s UI or API. In the ICC dashboard at https://icc.plt/, navigate to your Watt’s detail page — the Workflows tab shows runs in real time with status, version, and duration.

Deploy a new version while a workflow is running:

Terminal window
desk deploy --dir ./my-workflow-app \
--version v2 \
--hostname my-workflow-app.plt \
--profile skew-protection

The in-flight run on v1 continues executing on v1 pods. Queue messages are routed by deployment version — v1 messages go to v1 pods, v2 messages go to v2 pods. You can verify this in the ICC dashboard:

  1. Open the Watt detail page — the Deployments panel shows v2 as Active and v1 as Draining
  2. Open the Workflows tab — the running v1 run still shows Version: v1 and continues to completion
  3. Start a new workflow — it runs on v2

ICC expires v1 early only after its RPS is zero and the Workflow Service confirms there are no active runs, pending hooks, waiting sleeps, or queued messages for that version.

You can replay a completed workflow even while its version is draining. In the run detail view, click Replay — the new run targets the original deployment version (v1), not the latest. This proves that draining versions remain fully functional for workflow traffic.

Click any run in the Workflows tab to see:

  • Trace — waterfall of every step with timing bars and parallel execution
  • Graph — directed graph of the workflow structure
  • Events — raw event log with expandable payloads
  • Hooks — registered hooks/webhooks with status
  • Streams — data written via getWritable()

See the Workflows UI documentation for details.

Terminal window
desk cluster down --profile skew-protection