Table of contents
- Getting started
- Prerequisites
- Steps
- 1. Create an Nx workspace
- 2. Push to GitHub
- 3. Install dependencies
- 4. Generate the OpenShift CI pipeline
- 5. Generate the PEVN fullstack solution
- 6. Create the deploy secrets in each environment
- 7. Apply environment resources to OpenShift
- 8. Commit and push
- Configure sign-in redirect URIs per environment
- Local development
- Sandbox deployment
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
ocCLI authenticated to the cluster before running the pipeline generatorghCLI 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_SERVERandOPENSHIFT_TOKENas GitHub Actions secrets automatically - Prompts once for a GitHub classic PAT (
read:packages), creates the GHCR pull secret in OC, and links it to thegithub-actionsSA
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
devvalue is inapps/my-app-service/.env.localafter scaffolding. For test/prod, get the client secret from that environment’s ADSP tenant admin (Keycloak) for clienturn: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
oclogged in; podmaninstalled;gh auth loginas an account withwrite:packageson 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:
- 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.
- 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 tonx.json). - Imports the image into the namespace’s imagestream (
reference-policy=local, so pods pull from the internal registry) and applies the manifest. - 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