Up to date
Guide

Deploy with Vetra Apps

Connect a GitHub repository once. Every push to your production branch ships to production, and every pull request gets its own preview environment with Connect, Switchboard and your front-end.

How it works

An app links a GitHub repository to a production environment on Vetra. A GitHub Action in your repository builds the code, publishes your Powerhouse packages and (optionally) a front-end image, then asks Vetra to deploy those exact versions.

  1. 01git pushto main, a tag or a pull request
  2. 02GitHub Actionvetra-deploy-action builds your repo
  3. 03Renown tokenGitHub OIDC exchanged for a 10-min token
  4. 04Registry + Harborpackages published, image pushed
  5. 05Vetra deploypins the exact versions on the env
  6. 06Environmentsproduction, or the PR preview
No long-lived secrets in your repo: the action proves where it runs with GitHub's OIDC token, and Renown only trusts it for the repository and branches your app is linked to.

Quickstart

  1. 1

    Create an app and connect GitHub

    On vetra.io, install the Vetra Deploy GitHub App, pick your repository and choose the production branch. Vetra creates the production environment for you, or attaches one you already have.
  2. 2

    Authorize the deploy identity

    Your app gets its own Renown identity. Approve it on Renown; for the next year GitHub Actions in that repository can deploy on your behalf, and nothing else can. vetra.io reminds you 30 days before it expires.
  3. 3

    Add .github/workflows/vetra.yml

    Let Vetra open the setup pull request, or copy the workflow below. Merge it.
  4. 4

    Push to main

    The action publishes your packages and deploys production. The run summary links to the live URLs.
  5. 5

    Open a pull request

    A preview environment spins up for the PR and a comment links to it. Closing the PR removes it.

The workflow

This is the file the setup pull request adds (and the one ph init scaffolds). Replace <APP_ID>with the id from your app's Settings tab; the app page shows the snippet with it filled in.

.github/workflows/vetra.yml
name: Vetra
on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    types: [opened, synchronize, reopened]
permissions:
  id-token: write
  contents: read
concurrency:
  group: vetra-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true
jobs:
  deploy:
    if: github.event.pull_request.head.repo.fork != true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - uses: powerhouse-inc/vetra-deploy-action@v1
        with:
          app-id: <APP_ID>

id-token: write lets the job request a GitHub OIDC token, which Renown exchanges for a short-lived deploy token. No secrets need to be stored in the repository.

Inputs

InputDefaultDescription
app-idrequiredYour app id, shown on the app’s Settings tab.
package-dirs.Directories with a package.json to publish, separated by spaces or new lines. Empty publishes nothing.
build-commandpnpm buildRuns before publishing. Empty skips the build.
install-commandpnpm install --frozen-lockfileInstalls dependencies.
production-branchmainPushes to this branch deploy production.
fusion-dockerfile(empty)Path to a Dockerfile for a Next.js front-end. Empty = no image.
fusion-context.Docker build context for the front-end image.
fusion-image-nameappImage name inside your app’s registry project.
waittrueWait until the deployment is READY or FAILED.
timeout-minutes15How long to wait.
vetra-urlhttps://switchboard.vetra.ioVetra API.
renown-urlhttps://switchboard.renown.vetra.ioRenown token exchange.

Outputs

OutputDescription
deployment-idThe deployment created by this run.
environment-urlThe environment page on vetra.io.
app-urlThe deployed front-end or Connect URL.
versionThe package version that was published.

Monorepos

By default the action publishes the package at the repository root. In a monorepo, list every package directory to publish with package-dirs. Each one is packed with pnpm pack, so workspace:* and catalog: dependencies resolve to real versions.

.github/workflows/vetra.yml
      - uses: powerhouse-inc/vetra-deploy-action@v1
        with:
          app-id: <APP_ID>
          package-dirs: |
            packages/document-models
            packages/editors

Every listed package is published by the same run and installed on the environment at exactly that version. Set package-dirs to an empty string for a front-end-only app.

Front-ends (FUSION)

A FUSION service is a Next.js front-end that runs next to the environment's Connect and Switchboard. Point the action at its Dockerfile: it builds the image, pushes cr.vetra.io/<app project>/<image>:sha-<12 hex>to your app's private registry project and deploys that tag.

.github/workflows/vetra.yml
      - uses: powerhouse-inc/vetra-deploy-action@v1
        with:
          app-id: <APP_ID>
          fusion-dockerfile: apps/frontend/Dockerfile
          fusion-image-name: frontend

Image contract

  • output: 'standalone' in next.config. The server listens on PORT (3000) and / answers 2xx/3xx.
  • Build every NEXT_PUBLIC_* variable with the placeholder value __NEXT_PUBLIC_<NAME>__. One image then serves every environment.
  • Start through ph-fusion-entrypoint.sh (vendor it unchanged). At start it swaps each placeholder for the real value.
  • Run as uid/gid 1000.
  • Compare NEXT_PUBLIC_*flags at runtime, not at module scope, and don't call new URL(...) on them at import time: the placeholder is what the build sees.
Variable set by VetraValue
NEXT_PUBLIC_SWITCHBOARD_URLThe environment’s Switchboard GraphQL URL
NEXT_PUBLIC_CONNECT_URLThe environment’s Connect URL
NEXT_PUBLIC_RENOWN_URLhttps://www.renown.id
NEXT_PUBLIC_BASE_URLThe front-end’s own URL
apps/frontend/Dockerfile
FROM node:24-alpine AS build
WORKDIR /repo
RUN corepack enable
COPY . .
RUN pnpm install --frozen-lockfile
ENV NEXT_PUBLIC_SWITCHBOARD_URL=__NEXT_PUBLIC_SWITCHBOARD_URL__ \
    NEXT_PUBLIC_CONNECT_URL=__NEXT_PUBLIC_CONNECT_URL__ \
    NEXT_TELEMETRY_DISABLED=1
RUN pnpm --filter frontend build

FROM node:24-alpine
WORKDIR /app
COPY --from=build /repo/apps/frontend/.next/standalone ./
COPY --from=build /repo/apps/frontend/.next/static ./apps/frontend/.next/static
COPY --from=build /repo/apps/frontend/public ./apps/frontend/public
COPY ph-fusion-entrypoint.sh /ph-fusion-entrypoint.sh
USER 1000:1000
ENV FUSION_APP_DIR=/app PORT=3000 HOSTNAME=0.0.0.0 NODE_ENV=production
ENTRYPOINT ["/ph-fusion-entrypoint.sh"]
CMD ["node", "apps/frontend/server.js"]

Versions & dist-tags

The action derives the version from version in package.json (without any prerelease part) and never commits back to your repository.

TriggerVersionRegistrydist-tagDeploys
Pull request #42<base>-pr.42.<sha7>registry.dev.vetra.iopr-42Preview
Push to main<base>-main.<run>.<sha7>registry.vetra.iomainProduction
Tag v1.4.01.4.0registry.vetra.iolatestNothing (publish only)

Pull-request builds can only reach registry.dev.vetra.io and their own preview: a PR can never publish to the production registry or deploy production.

Preview environments

  • One environment per open pull request, created on its first deploy.
  • Slim by design: a small database without backups, the smallest service sizes, and packages from registry.dev.vetra.io. Data starts empty.
  • Deleted when the pull request is closed or merged.
  • At most 5 per app by default; at the limit the least recently deployed preview makes room. Previews idle for 7 daysare removed. Both are adjustable in the app's Settings.
  • No secrets: previews get your front-end's non-secret variables only, which is also the safe default for untrusted branches.
  • Pull requests from forksdon't get previews: GitHub withholds the OIDC token from fork workflows.

Rollback

Open your app's Deployments tab and choose Rollback on any earlier successful production deployment. Vetra re-applies its exact package versions and image tag as a new deployment. The next push to the production branch deploys over it again, so revert the bad commit too.

Troubleshooting

“Unable to get ACTIONS_ID_TOKEN_REQUEST_URL” or no OIDC token

The job lacks permissions: id-token: write. Add it at the workflow or job level. Fork pull requests never get a token, so they are skipped by the if: condition in the template.

Token exchange refused: identity not authorized

The app's deploy identity hasn't been approved yet. Open the app on vetra.io, choose Authorize on Renown, sign, then Check again. The exchange also refuses refs other than the production branch, v* tags and pull requests.

IDENTITY_EXPIRED: deploy identity expired

The Renown authorization of the app's deploy identity is valid for a year. Once it lapses the action fails with IDENTITY_EXPIRED and CI deploys pause; what is already running keeps running. Open the app on vetra.io, choose Re-authorize, sign on Renown, and re-run the failed workflow. The app page warns you 30 days ahead.

Publish fails with 403 on the registry

Package names on the Vetra registry belong to the Renown address that first published them. The deploy identity publishes as you, so your existing packages keep working; a name owned by another account is rejected. Rename the package (for example under your own scope) or ask its owner to publish it.

Deployment FAILED or timed out

The Deployments tab shows the error, and the Logs link opens the GitHub Actions run. For runtime errors, open the environment page from the app and check the service logs.