Skip to content

Usage Examples

Scenario 1: CLI workflow (CI token)

bash
# 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=prod

Scenario 2: Test → prod workflow

bash
# 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=test

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

yaml
# .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.

javascript
// 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.

javascript
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 it

2 — Web component via a side-effect import. The module runs customElements.define(...) when imported; configure it by setting properties (or attributes) on the element.

javascript
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.)

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

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

javascript
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 of import() — same result, different style.

Scenario 4: New version has a bug → rollback

bash
# 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=test

Scenario 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:

bash
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... --protected

A 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?":

bash
# 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=20

history 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

bash
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>":

bash
# 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

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

Packdog runs on Cloudflare Workers, D1, and R2.