Heroku Review Apps Architecture Note: Isolated Per-PR Previews
A concise architecture note for Heroku Review Apps covering requirements, minimal pipeline design, trust boundaries, operational checks, failure modes and when to change the design.
13 Dec 2025, 06:01 UTC

Problem: per-PR previews without manual provisioning or data leakage
Teams want a live URL for every pull request without creating apps by hand, while keeping code from untrusted contributors isolated from production data and secrets. The smallest safe Heroku design uses a Pipeline with Review Apps, per-app dynos, and optional per-app Postgres, with config vars inherited from a staging app and automatic teardown on PR close.
Requirements for per-PR preview
Automatic creation of a temporary dyno environment for each pull request with isolated build, runtime and optional database. Integration with GitHub for CI/CD so a Review App is created on open and destroyed on merge or close. The environment must use the same stack and buildpack as staging, inherit non-secret configuration, and be reachable via a unique hostname.
Dyno is Heroku's container-like process unit. Buildpack is the build system that compiles slug from source. Config vars are environment variables stored by Heroku and injected at runtime.
Minimal suitable design
A Heroku Pipeline with one staging app. Review Apps are enabled on the pipeline and tied to the GitHub repository. Each PR triggers a new app named by the pipeline, with its own dyno formation and, if enabled, its own Postgres add-on provisioned at create time.
Config vars inherit from staging. Secrets remain in config vars and are not written to the repository. The Review App receives a copy of the vars at creation; changes to staging vars do not retroactively update existing Review Apps.
Example configuration surface:
heroku pipelines:open --app <staging-app>Run from a terminal with a Heroku CLI authenticated user that has access to the pipeline. The command lists pipeline apps. Risk: exposing pipeline membership to anyone with read access.
Review App lifecycle is managed by Heroku. Manual intervention is limited to setting the Review Apps config in the pipeline: enable/disable, which branches trigger, and whether a Postgres add-on is auto-provisioned.
Trust and data boundaries
Code from untrusted PRs runs in isolated dynos. The dyno can only access resources explicitly granted via config vars and add-ons attached to that Review App. Database credentials are scoped to the Review App's Postgres instance and are not shared with staging or production.
Secrets are stored in config vars, not in the slug. The PR code cannot read Heroku's dashboard or API. Log drains forward logs out of the dyno to a central aggregator; ensure the drain destination filters sensitive data because config vars are not automatically masked in logs.
Do not attach a shared staging or production database to Review Apps. If a shared database is required, enforce role isolation and separate schemas, and accept the increased blast radius.
Operational checks
Health: rely on Heroku's router health checks and the app's own readiness endpoint. Monitor dyno restarts and boot time.
Logs: configure a log drain to a central aggregator. Verify the Review App appears in the drain stream after creation.
Metrics: use Heroku Metrics for dyno uptime and response latency. Set alerts for repeated dyno restarts and build failures.
Verification steps you can run manually:
- Create a GitHub PR against the pipeline repo and confirm a Review App URL is posted to the PR.
- Run
heroku apps:info --app <review-app-name>with a user that has read access to the app. Expected check: app exists and is linked to the pipeline. - Run
heroku config --app <review-app-name>with read config permission. Expected check: config vars match the inherited set from staging, without production secrets. - Inspect the database connection string in config vars to confirm it points to the intended Postgres add-on for that Review App.
Risks: viewing config vars can expose secrets in terminal history. Use a secure session and avoid copying values.
Failure modes
Build failures due to missing dependencies or buildpack incompatibilities. Build timeouts when the build process is heavy.
Dyno crashes from memory limits. Heroku dynos have a memory quota; exceeding it causes R14 and restart.
Database connection timeouts when the per-app Postgres is under-provisioned or the app opens too many connections.
Resource limits such as dyno hours exhaustion on free tiers, and database credit consumption from per-PR Postgres instances.
Conditions that change the design
Persist data across Review Apps: use a shared database with schema isolation or a data migration step. This increases trust risk.
External service integration: add necessary add-ons or API keys via pipeline config vars, and accept that secrets are now present in Review Apps.
Performance constraints: increase dyno size or enable autoscaling for the Review App formation.
Need production-like data: use Heroku Shield or a dedicated test database with sanitized data, rather than per-PR empty databases.
Limitations: Review Apps are for temporary isolated testing, not production workloads. Database add-ons consume credits. Build times can be long without caching. Secrets in config vars are not automatically masked in logs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.