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.
- 01git pushto main, a tag or a pull request
- 02GitHub Actionvetra-deploy-action builds your repo
- 03Renown tokenGitHub OIDC exchanged for a 10-min token
- 04Registry + Harborpackages published, image pushed
- 05Vetra deploypins the exact versions on the env
- 06Environmentsproduction, or the PR preview
Quickstart
- 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
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
Add .github/workflows/vetra.yml
Let Vetra open the setup pull request, or copy the workflow below. Merge it. - 4
Push to main
The action publishes your packages and deploys production. The run summary links to the live URLs. - 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.
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
| Input | Default | Description |
|---|---|---|
app-id | required | Your 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-command | pnpm build | Runs before publishing. Empty skips the build. |
install-command | pnpm install --frozen-lockfile | Installs dependencies. |
production-branch | main | Pushes 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-name | app | Image name inside your app’s registry project. |
wait | true | Wait until the deployment is READY or FAILED. |
timeout-minutes | 15 | How long to wait. |
vetra-url | https://switchboard.vetra.io | Vetra API. |
renown-url | https://switchboard.renown.vetra.io | Renown token exchange. |
Outputs
| Output | Description |
|---|---|
deployment-id | The deployment created by this run. |
environment-url | The environment page on vetra.io. |
app-url | The deployed front-end or Connect URL. |
version | The 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.
- 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.
- 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'innext.config. The server listens onPORT(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 callnew URL(...)on them at import time: the placeholder is what the build sees.
| Variable set by Vetra | Value |
|---|---|
NEXT_PUBLIC_SWITCHBOARD_URL | The environment’s Switchboard GraphQL URL |
NEXT_PUBLIC_CONNECT_URL | The environment’s Connect URL |
NEXT_PUBLIC_RENOWN_URL | https://www.renown.id |
NEXT_PUBLIC_BASE_URL | The front-end’s own URL |
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.
| Trigger | Version | Registry | dist-tag | Deploys |
|---|---|---|---|---|
| Pull request #42 | <base>-pr.42.<sha7> | registry.dev.vetra.io | pr-42 | Preview |
| Push to main | <base>-main.<run>.<sha7> | registry.vetra.io | main | Production |
| Tag v1.4.0 | 1.4.0 | registry.vetra.io | latest | Nothing (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.