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.

The skew-protection profile installs Envoy Gateway as the Gateway API controller, creates a Gateway resource, and enables skew protection on ICC with shorter timeouts suited for testing.

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

Terminal window
desk cluster up --profile skew-protection

This 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.

The skew-protection profile enables skew protection with testing-friendly defaults. These values can be customized by editing the profile file:

SettingProfile DefaultProduction DefaultDescription
skew_protection.enabletruefalseEnables skew protection
skew_protection.http_grace_period_ms30000 (30 sec)1800000 (30 min)Grace period — version kept alive unconditionally
skew_protection.http_max_alive_ms180000 (3 min)86400000 (24h)Max alive — hard ceiling before force-expiration
skew_protection.check_interval_ms10000 (10 sec)60000 (1 min)How often ICC checks if draining versions can be expired
skew_protection.traffic_window_ms60000 (1 min)300000 (5 min)Prometheus query window for traffic measurement
skew_protection.cookie_max_age43200 (12h)43200 (12h)Browser cookie lifetime in seconds
skew_protection.auto_cleanupfalsefalseWhether to delete expired Deployments and Services

Use the --version flag to deploy a versioned application. Desk automatically sets the correct app.kubernetes.io/name and plt.dev/version labels:

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/. Use the browser’s developer tools (Network tab) to check that the response includes a Set-Cookie: __plt_dpl=v1; ... header. Your browser is now pinned 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

Existing session — refresh the page in the same browser. Since the __plt_dpl=v1 cookie is still set, your requests continue to be routed to version 1. You can verify this in the developer tools — no new Set-Cookie header is returned.

New session — open a new incognito/private window and navigate to https://svcs.gw.plt/my-app/. This simulates a new visitor with no cookie. The response should include Set-Cookie: __plt_dpl=v2; ..., pinning this session to version 2.

Draining versions are expired automatically when ICC detects zero traffic (after the grace period) or when the max-alive ceiling is reached. With the testing profile’s short timeouts (30-second grace period, 3-minute max alive), this happens fairly quickly.

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, any browser still holding the old __plt_dpl=v1 cookie will be routed to version 2 on the next request, and a new Set-Cookie: __plt_dpl=v2 replaces the stale cookie.

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 — new visitors get pinned to v2 via the __plt_dpl cookie, while existing sessions on v1 continue to be routed there 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 only expires the v1 version when the Workflow Service confirms there are no active runs, pending hooks, waiting sleeps, or queued messages for v1.

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