Table of contents
  1. Getting started
    1. Prerequisites
      1. OpenShift projects
      2. GitHub Actions secrets
      3. GHCR pull secret in OpenShift
      4. Database secret
      5. ADSP tenant
      6. GitHub Environments (optional)
      7. Local tooling
    2. Steps
      1. 1. Create an Nx workspace
      2. 2. Push to GitHub
      3. 3. Install dependencies
      4. 4. Generate the OpenShift CI pipeline
      5. 5. Generate the PEVN fullstack solution
      6. 6. Create the deploy secrets in each environment
      7. 7. Apply environment resources to OpenShift
      8. 8. Commit and push
      9. Configure sign-in redirect URIs per environment
    3. Local development
    4. Sandbox deployment
      1. Set up sandbox once
      2. Iterate
      3. Credentials and databases
      4. Teardown a sandbox app

Getting started

Step-by-step guide to creating a new Nx workspace, installing the plugins, and generating a PEVN (PostgreSQL + Express + Vue 3 + Node) fullstack solution with OpenShift deployment.

Prerequisites

OpenShift projects

All projects must exist before running the pipeline generator:

  • One infra project — holds ImageStreams and the CI service account (e.g. my-project-build)
  • One project per environment (e.g. my-project-dev, my-project-test, my-project-prod)

GitHub Actions secrets

OPENSHIFT_SERVER and OPENSHIFT_TOKEN are set automatically when the pipeline generator runs with Apply the pipeline to OpenShift? Yes (step 4). The following are set or needed manually:

Secret How set Notes
OPENSHIFT_SERVER Auto (step 4) API URL derived from current oc context
OPENSHIFT_TOKEN Auto (step 4) Token for the github-actions SA
REDHAT_IO_USERNAME Manual Only needed for UBI images from registry.redhat.io
REDHAT_IO_PASSWORD Manual Paired with REDHAT_IO_USERNAME

GITHUB_TOKEN is provided automatically by GitHub Actions — no setup required.

GHCR pull secret in OpenShift

The oc import-image step runs server-side: OpenShift pulls from GHCR directly, not via the Actions runner. The pull secret is created automatically when the pipeline generator runs with apply enabled — you will be prompted once for a classic PAT with read:packages scope. The secret is linked to the github-actions SA in the infra project and persists permanently.

To set it up manually (e.g. for an existing pipeline):

nx g @abgov/nx-oc:setup-secrets --infra my-project-build

Database secret

A Postgres connection secret must exist in each environment project before the first deploy:

oc create secret generic my-app-service-database \
  --from-literal=DATABASE_URL='postgresql://user:pass@host:5432/dbname' \
  -n <env-project>

Repeat for every environment project.

ADSP tenant

The pevn generator provisions Keycloak clients and environment config against an ADSP tenant. Have the tenant name ready if you already have one. If you don’t, run pevn without --tenant and its interactive login flow offers + Create a new tenant — available in dev/test (never prod) for eligible accounts; see nx-adsp’s Authentication section for the exact rules.

The generated public client allows local development (http://localhost:4200/*) out of the box. For deployed environments you must register each environment’s Route URL with the client — see Configure sign-in redirect URIs per environment.

GitHub Environments (optional)

Create environments in Settings → Environments matching the names passed to --envs (e.g. dev, test, prod) to enable per-environment approval gates.

Local tooling

  • oc CLI authenticated to the cluster before running the pipeline generator
  • gh CLI authenticated (gh auth login) before running the pipeline generator
  • Node 22+

Steps

1. Create an Nx workspace

npx create-nx-workspace@latest my-project --preset apps --nx-cloud false
cd my-project

2. Push to GitHub

git remote add origin <REMOTE_URL>
git push -u origin main

3. Install dependencies

npm i -D @nx/vue @nx/node @abgov/nx-oc @abgov/nx-adsp

@nx/vue and @nx/node are the required peers for the PEVN generators.

4. Generate the OpenShift CI pipeline

oc login <URL> --token=<TOKEN>

npx nx g @abgov/nx-oc:pipeline

Respond to the prompts:

? What should be the name of the oc pipeline?           my-project-ci
? What project should be used for build infrastructure?  my-project-build
? Generate a Jenkins or GitHub Actions pipeline?         actions
? What projects should be used for environments?         my-project-dev my-project-test my-project-prod
? Apply the pipeline to OpenShift?                       Yes

When Apply is Yes, the generator:

  • Applies OC infra manifests (ImageStream, RBAC, service account)
  • Sets OPENSHIFT_SERVER and OPENSHIFT_TOKEN as GitHub Actions secrets automatically
  • Prompts once for a GitHub classic PAT (read:packages), creates the GHCR pull secret in OC, and links it to the github-actions SA

This writes .github/pipeline.yml and .openshift/environments.yml. OpenShift deployment manifests are only auto-generated by the app generators when this pipeline setup is in place.

5. Generate the PEVN fullstack solution

npx nx g @abgov/nx-adsp:pevn my-app --env dev --tenant <TENANT>

A browser login opens for your ADSP tenant. This generates:

.openshift/
  ├─ my-app-service/
  └─ my-app-app/
apps/
  ├─ my-app-service/   ← Express + Drizzle (Postgres)
  └─ my-app-app/       ← Vue 3 + GoA web components + Keycloak

The dev proxy (/api/my-app-service:3333) and nginx production proxy are wired automatically.

Pass --skipAgent to skip the AI interaction and get base scaffolding only.

6. Create the deploy secrets in each environment

Database — a Postgres connection string:

oc create secret generic my-app-service-database \
  --from-literal=DATABASE_URL='postgresql://user:pass@host:5432/dbname' \
  -n my-project-dev

# Repeat for test and prod
oc create secret generic my-app-service-database \
  --from-literal=DATABASE_URL='postgresql://...' \
  -n my-project-test

oc create secret generic my-app-service-database \
  --from-literal=DATABASE_URL='postgresql://...' \
  -n my-project-prod

ADSP client secret — the service authenticates with the access service using its confidential Keycloak client secret. Scaffolding writes it to the service’s .env.local (CLIENT_SECRET=...) for the generator’s tenant; each environment runs against its own tenant, so create the Secret per environment with that environment’s client secret:

oc create secret generic my-app-service-secrets \
  --from-literal=CLIENT_SECRET='<client-secret-for-this-environment>' \
  -n my-project-dev

# Repeat for test and prod (each has its own client secret)

The dev value is in apps/my-app-service/.env.local after scaffolding. For test/prod, get the client secret from that environment’s ADSP tenant admin (Keycloak) for client urn:ads:<tenant>:my-app-service.

The sandbox deploy (nx run my-app-service:sandbox) creates this Secret automatically from the service’s .env.local — the manual step is only for the GitHub Actions / apply-envs environments.

7. Apply environment resources to OpenShift

npx nx run my-app-service:apply-envs
npx nx run my-app-app:apply-envs

8. Commit and push

git add .
git commit -m "feat: scaffold PEVN fullstack solution"
git push

The GitHub Actions pipeline triggers on push to main and builds, publishes, and deploys to the first environment automatically.

Configure sign-in redirect URIs per environment

The frontend’s public Keycloak client is created during scaffolding with only http://localhost:4200/* as an allowed redirect URI (for local development). Browser sign-in on a deployed app fails with invalid redirect_uri until the deployed Route URL is added to that client.

This is a manual, one-time step per environment, because each environment runs against a different ADSP tenant on a different Keycloak instance:

Environment ADSP access (Keycloak)
dev access.adsp-dev.gov.ab.ca
test (UAT / pre-prod) access-uat.alberta.ca
prod access.alberta.ca

Since pre-production and production are separate tenants on separate Keycloak instances, the generator (which authenticates against a single tenant) cannot configure them all — you register each one after its first deploy.

For each environment, open that environment’s ADSP tenant admin (Keycloak) and edit the app’s public client — urn:ads:<tenant>:<app> (e.g. urn:ads:my-tenant:my-app-app). Add the deployed Route URL:

https://<app>-<environment-project>.<cluster-apps-domain>/*

for example https://my-app-app-my-project-dev.apps.<cluster>/* — to both:

  • Valid redirect URIs
  • Valid post logout redirect URIs

Leave Web origins as + (it already allows CORS from the redirect-URI origins). The exact Route host for a deployed app is shown by oc get route <app> -n <environment-project>.

The sandbox deployment flow does this automatically — it targets a single tenant/Keycloak, so the generator registers the sandbox Route on the client for you. The manual step above applies only to the pipeline-deployed environments.


Local development

Start the local Postgres container (requires Podman):

npx nx run my-app-service:dev-db

Generate and apply the initial migration (only needed once you have added a table to src/db/schema.ts):

npx nx run my-app-service:db:generate   # writes SQL to drizzle/
npx nx run my-app-service:db:migrate    # applies it to the local DB

Run the service and frontend concurrently:

npx nx run my-app-service:serve
npx nx run my-app-app:serve

The Vue dev server proxies /api/ to the local Express service automatically via vite.proxy.json.


Sandbox deployment

For rapid iteration directly on OpenShift without committing and waiting for CI. Prerequisites:

  • an existing OpenShift namespace (per-user) and oc logged in;
  • podman installed;
  • gh auth login as an account with write:packages on your GHCR org (the same login backs both the image push and the pull secret — no PAT is stored).

Set up sandbox once

npx nx g @abgov/nx-oc:sandbox my-app-service --sandboxProject my-project-sandbox --database postgres
npx nx g @abgov/nx-oc:sandbox my-app-app --sandboxProject my-project-sandbox

This generates sandbox manifests under .openshift/my-app-service/ and .openshift/my-app-app/, a shared sandbox-postgres Deployment + Service + PVC under .openshift/sandbox/, and adds sandbox and sandbox-teardown targets to each project.

Iterate

npx nx run my-app-service:sandbox
npx nx run my-app-app:sandbox

Each command:

  1. Provisions the shared Postgres instance (CloudNativePG operator when present, plain Deployment otherwise), creates the per-app database, and creates the app’s ADSP client Secret. Idempotent.
  2. Builds the image locally (podman build --platform=linux/amd64) and pushes it to <registry>/<sandboxProject>-<app>:sandbox (registry resolved from the git remote on first run, persisted to nx.json).
  3. Imports the image into the namespace’s imagestream (reference-policy=local, so pods pull from the internal registry) and applies the manifest.
  4. Restarts the Deployment and waits for rollout.

No GitHub push, no CI wait — and local layer caching means only the changed app layer re-uploads, so repeat iterations are fast.

Credentials and databases

The shared sandbox-postgres Postgres instance is deployed once and reused by all apps in the namespace. On GoA ARO, the executor provisions it via the CloudNativePG operator (a CNPG Cluster CR backed by an azure-disk PVC, with per-app Database CRs). When the operator is absent it falls back to a plain Deployment. Either way, credentials are exposed as a sandbox-postgres-app Secret (keys: username, password) and the service endpoint is sandbox-postgres-rw:5432. No credential is hardcoded in any manifest.

Each app gets its own database within the shared instance (my-app-service_sandbox, my-app-app_sandbox). Migrations are applied automatically on every deploy by the service’s migrate.js init container.

Teardown a sandbox app

To remove a single app from the sandbox namespace without touching the shared database:

npx nx run my-app-service:sandbox-teardown

This runs oc delete all,configmap -l app=<name> against the sandbox namespace, removing every resource the template created. The shared sandbox-postgres instance is left in place — other apps in the namespace may still be using it.

To also remove the shared database (CNPG path):

# Remove per-app Database CRs first
oc delete -f .openshift/sandbox/my-app-service-db.yml -n my-project-sandbox
# Then remove the shared Cluster (also deletes the PVC and CNPG-managed secrets)
oc delete -f .openshift/sandbox/sandbox-postgres-cnpg.yml -n my-project-sandbox

For the plain Deployment fallback:

oc delete -f .openshift/sandbox/sandbox-postgres.yml -n my-project-sandbox
oc delete secret sandbox-postgres-creds sandbox-postgres-app -n my-project-sandbox
oc delete service sandbox-postgres-rw -n my-project-sandbox

This site uses Just the Docs, a documentation theme for Jekyll.