Usage Examples
Scenario 1: CLI workflow (CI token)
# Sign in via the browser. Session saved to ~/.config/packdog/config.json.
packdog login
packdog whoami # confirm it worked
# List the packages you have access to
packdog packages
# → checkout-widget pkg_8Kd2mZqR3nT5vWxYbCdE
# Link this directory to a package (a name or a pkg_ id — link resolves it to the stored id)
packdog link checkout-widget
# Build (project-specific)
npm run build
# The test and prod channels must already exist before you can deploy to them.
# An organization admin creates them once (see Scenario 5).
# A typo'd channel name fails with a 404 instead of creating a junk channel.
# Ship the build — upload dist/ and publish to the test channel
packdog deploy --channel=test --dir=dist
# → Uploaded: ver_7Hq2Np4mR8Kt (3 files, 45.2 KB)
# → Published to test: ver_7Hq2Np4mR8Kt
# Check versions and channels
packdog versions
packdog channels
# Promote that version to prod when ready
packdog promote --to=prod --from=test
# Roll back if something goes wrong
packdog rollback --channel=prodScenario 2: Test → prod workflow
# Build and publish to the test channel in one step
packdog deploy --channel=test --dir=dist
# Test against the test channel...
# When ready: promote that version to prod
packdog promote --to=prod --from=testScenario 2b: Tag a version with its source (in CI)
Record which release and which commit a version came from, so you can tell at a glance what's live — and click straight through to the code. Wire --label and --commit to what your CI platform gives you; the CLI never runs git itself.
# .github/workflows/ship.yml (excerpt)
- run: npm run build
- run: |
packdog deploy \
--channel=prod \
--dir=dist \
--label="${GITHUB_REF_NAME}" \
--commit="${GITHUB_SHA}"
env:
PACKDOG_TOKEN: ${{ secrets.PACKDOG_TOKEN }}Set the package's Repository in the web panel (Package → Repository) to a GitHub URL and each version's commit becomes a link straight to the source. packdog versions and packdog history show label · commit on every row.
Scenario 3: Client loads a package
Resolve the channel once to get baseUri — the root URL of the current version's immutable files — then consume those files however suits you. baseUri always points at the channel's live version, so loads pick up a publish or rollback with no change to this code, and relative asset paths inside the bundle resolve against it automatically. There's no required entry filename: you load whatever you shipped (index.js, main.js, index.html, …).
Packdog only serves the files — how you load them is entirely up to you. The patterns below are just a few illustrative options, not a fixed menu; they run from most-integrated (call exported functions, share the page's DOM) → shadow DOM (isolated styles, shared JS) → iframe (fully sandboxed). Mix and match — nothing here is prescribed.
// Resolve the channel once — every pattern below starts from this baseUri.
const { baseUri } = await fetch(
'https://api.packdog.dev/v1/packages/PACKAGE_ID/channels/prod',
{ headers: { 'Authorization': 'Bearer load_yourcustomerkey' } }
).then(r => r.json());1 — Call an exported startup function (most explicit). The module exports a mount() (or a default init()); capture the namespace and call it with whatever your bundle's API takes — a target node, config, callbacks. Pass baseUri through so the module can load its own runtime assets.
const mod = await import(`${baseUri}/index.js`);
const widget = mod.mount(document.getElementById('root'), {
baseUri,
locale: 'nb-NO',
theme: 'dark',
onReady: () => console.log('ready'),
onEvent: (e) => console.log(e),
});
widget.destroy?.(); // later, if the API offers it2 — Web component via a side-effect import. The module runs customElements.define(...) when imported; configure it by setting properties (or attributes) on the element.
await import(`${baseUri}/index.js`);
const el = document.createElement('my-widget');
el.config = { locale: 'nb-NO', theme: 'dark' };
el.baseUri = baseUri;
document.getElementById('root').appendChild(el);3 — Shadow DOM for isolation. Render inside a shadow root so the embed's styles and DOM are isolated from the host page (and the host's CSS can't leak in). Give the module a host — let it attach the shadow root, or attach it yourself and pass it as the mount target. (baseUri is extra useful here: the module needs it to pull its own CSS into the shadow root.)
const host = document.getElementById('root');
const root = host.attachShadow({ mode: 'open' });
const mod = await import(`${baseUri}/index.js`);
mod.mount(root, { baseUri, locale: 'nb-NO' });4 — iframe (fully sandboxed) with parameters. Maximum isolation: a separate document with its own JS context. Pass params in the URL, or postMessage after load for richer data.
const frame = document.createElement('iframe');
frame.src = `${baseUri}/index.html?locale=nb-NO&theme=dark`;
frame.addEventListener('load', () => {
frame.contentWindow.postMessage({ type: 'init', apiKey: '…' }, new URL(baseUri).origin);
});
document.body.appendChild(frame);5 — Plain ES module, no UI. It's just an ES module — if your bundle is a library, import what it exports and call it. No DOM required.
const { render } = await import(`${baseUri}/index.js`);
const html = render({ items });Prefer side-effect loading without
await? Inject a<script type="module" src="${baseUri}/index.js">tag instead ofimport()— same result, different style.
Scenario 4: New version has a bug → rollback
# Build, deploy to test, then promote to prod
packdog deploy --channel=test --dir=dist
packdog promote --to=prod --from=test
# A critical bug — roll back immediately
packdog rollback --channel=prod
# → Rolled back to: <previous versionId>
# Fix the bug, rebuild, deploy, promote
npm run build
packdog deploy --channel=test --dir=dist
packdog promote --to=prod --from=testScenario 5: Organization admin sets up a package and its channels
Channels are explicit resources — a CI token's deploy/promote can only target a channel that already exists. An organization admin creates the package and its channels up front:
packdog login # browser sign-in — sign in as an admin of your organization
# Create the package and its channels in one step
packdog packages create promo-banner --channels=test,prod
# → Created package promo-banner
# → ID: pkg_4Qj7nLpV2sR9...
# → Channels: test, prod
# Add another channel to an existing package later (max 10 per package)
packdog channels create stage -p pkg_4Qj7nLpV2sR9...
# Protect a channel so only privileged CI tokens / member humans can publish to it
packdog channels create release -p pkg_4Qj7nLpV2sR9... --protectedA freshly-created channel is empty until a version is published — packdog channels shows it as (empty). CI tokens can now packdog deploy --channel=test --dir=dist against it.
Scenario 6: See who changed what
Two read-only logs answer "what happened to this package?":
# A channel's publish history — every version published to it, newest first
packdog history --channel=prod
# 2026-05-17 10:30:00 ver_7Hq2Np4mR8Kt... ← current
# 2026-05-16 14:02:00 ver_3pLm9WqV2sB...
# The audit log — every mutating action, with the actor behind it
packdog audit
# 2026-05-17 10:30:00 channel.publish alice@acme.com → prod {versionId=ver_7Hq2Np4mR8Kt}
# 2026-05-17 10:28:00 version.upload alice@acme.com → ver_7Hq2Np4mR8Kt...
packdog audit --limit=20history is per channel and shows versions only; audit is per package and shows who did it. Same commands for admins — pass --package=<id> when working across packages without a packdog.json.
Direct-curl examples
These mirror what the CLI does — useful if you want to call the API directly from a CI script.
Upload a version
curl -X POST https://api.packdog.dev/v1/packages/$PACKAGE_ID/upload \
-H "Authorization: Bearer $PACKDOG_TOKEN" \
-F "index.js=@dist/index.js" \
-F "assets/ship.png=@dist/assets/ship.png"
# → { "versionId": "ver_7Hq2Np4mR8Kt", ... }Publish a version to a channel
Two body shapes — pick one. The first pins a specific version (deploy semantics); the second copies whatever the source channel currently points at (promote semantics) and records that source in the audit log so the activity view can show "Promoted from <test>":
# Deploy: explicit version
curl -X POST https://api.packdog.dev/v1/packages/$PACKAGE_ID/channels/prod \
-H "Authorization: Bearer $PACKDOG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"versionId": "ver_7Hq2Np4mR8Kt"}'
# Promote: copy what test currently points at
curl -X POST https://api.packdog.dev/v1/packages/$PACKAGE_ID/channels/prod \
-H "Authorization: Bearer $PACKDOG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fromChannel": "test"}'Sending both, or neither, returns 400. A fromChannel that doesn't exist returns 404; one that exists but is empty returns 409. Publishing the same version a channel is already on returns 409 ("already at this version" — prevents a duplicate publish-history entry that would make rollback look like a no-op).
The prod channel must already exist — publishing returns 404 ("Channel 'prod' does not exist — create it first") otherwise. If prod is a protected channel, the CI token also needs canPublishProtected = true on its grant for the package's project. If prod is locked ({"frozen": true} set via PATCH), every publish + rollback returns 409 regardless of caller — unlock first via PATCH {"frozen": false}.
Roll back the current channel
curl -X POST https://api.packdog.dev/v1/packages/$PACKAGE_ID/channels/prod/rollback \
-H "Authorization: Bearer $PACKDOG_TOKEN"Returns 409 Conflict if another publish raced you — retry once. Rolling back a protected channel also requires canPublishProtected = true on the CI token's grant for the package's project.