Github
Registry

Connect a workspace

Give a workspace a registry URL and a credential, with a device login or an access token, locally and in CI.

A connected workspace publishes to a registry and checks content out of it. It needs a registry URL and a credential. A device login and an access token give the same kind of credential.

Device login

Terminal
npm run login -- --url https://registry.example.com
  1. The CLI shows a device code and opens the registry's approval page.
  2. Sign in to the registry, enter the code, and approve the CLI.
  3. The CLI saves the credential in .handoff/cli-auth.json for that exact registry URL.
  • Include the base path in --url if the registry has one. Without --url, the CLI uses the configured registry URL.
  • --no-browser, or HANDOFF_LOGIN_NO_BROWSER=true, prints the approval URL without opening it.
  • The login carries the scopes of your role. Publishing needs an administrator.

Several registries

The CLI saves one login per profile:

Terminal
npm run login -- --profile staging --url https://staging.example.com
npm run publish -- all --profile staging

A login profile name needs no handoff.config.<name>.* file, and login is the only command that accepts a new name. A profile without its own login uses the default login.

Sign out

Terminal
npm run logout
npm run logout -- --profile staging
npm run logout -- --all

logout revokes the saved token when it can, then removes the login of the selected profile. --url limits it to a login for that exact URL. --all removes every saved login.

Access token

Create a token in the registry under Account → Access tokens. Publishing needs a Read/write token; checkout needs Read. See Accounts and access.

.env
HANDOFF_REGISTRY_URL=https://registry.example.com
HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...

These two variables are the defaults of runtime.registryConnection, so no config is needed. A selected profile reads .env.<profile> on top of .env, so each profile can point at a different registry. Add a config block only to keep the URL in the repository or to read different variable names:

handoff.config.ts
runtime: {
  registryConnection: {
    url: 'https://registry.example.com',
    accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
  },
},

Which credential wins

publish and checkout look for a credential in this order:

  1. HANDOFF_REGISTRY_URL and HANDOFF_REGISTRY_ACCESS_TOKEN, or runtime.registryConnection.
  2. The saved login of the selected profile, if it belongs to the same registry URL. A profile without a login uses the default login.
  3. HANDOFF_SYNC_SECRET, when there is no token and no login.

Environment values win over a saved login, so a CI job behaves the same on a machine where a developer is signed in. If the environment names a different registry than the profile's login, publish and checkout say which login they skipped and why.

Publish from CI

.github/workflows/publish.yml
name: Publish to registry
on:
  push:
    branches: [main]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run publish -- all
        env:
          HANDOFF_REGISTRY_URL: ${{ vars.HANDOFF_REGISTRY_URL }}
          HANDOFF_REGISTRY_ACCESS_TOKEN: ${{ secrets.HANDOFF_REGISTRY_ACCESS_TOKEN }}
          HANDOFF_FIGMA_PROJECT_ID: ${{ vars.HANDOFF_FIGMA_PROJECT_ID }}
          HANDOFF_DEV_ACCESS_TOKEN: ${{ secrets.HANDOFF_DEV_ACCESS_TOKEN }}
  • The token needs read and write access, so an administrator creates it.
  • publish tokens and publish assets run the Figma pipeline, so all needs the Figma variables. To publish only catalog items and pages, run publish catalog and publish pages and drop the Figma variables.

On this page