Architecture Note: Using Heroku Review Apps for Pull‑Request Environments
How to design a minimal, trust‑bounded Review App setup that stays within dyno limits, isolates data, and cleans up automatically when a PR closes.
17 Aug 2025, 20:48 UTC

Problem and Takeaway
Teams that rely on pull‑request (PR) previews often need a production‑like environment that spins up on demand, mirrors the parent app’s configuration, and disappears when the PR is closed. Manually provisioning staging apps leads to orphaned resources, configuration drift, and unexpected costs. The takeaway is to use Heroku Review Apps as the smallest suitable design that satisfies these requirements while keeping trust boundaries clear and operational overhead low.
Requirements
- Parity: Review Apps must use the same buildpacks, config vars, and add‑on plans as the parent app.
- Isolation: Each PR gets its own dyno formation and, if enabled, a separate Heroku Postgres database.
- Automatic lifecycle: The app is created when the PR opens and destroyed when the PR is closed, merged, or the branch is deleted.
- Resource governance: Slug size must stay under 500 MB, dyno consumption must remain within the parent app’s pool, and external services must not receive unintended test traffic.
- Observability: Operators need to verify dyno formation, add‑on attachment, and slug size before and after a PR lifecycle.
Smallest Suitable Design
The design consists of three components:
- Parent Heroku app – the canonical production‑like application that defines buildpacks, config vars, and optional add‑ons (e.g., Heroku Postgres hobby‑dev).
- Review App – a child app spawned by Heroku’s platform when a linked GitHub PR is opened. It inherits the parent’s slug, config vars (excluding those marked as
SECRET), and add‑on plans. - GitHub repository – the source of truth; PR events trigger the Review App lifecycle via the Heroku‑GitHub integration.
No additional tooling (e.g., custom scripts, external orchestrators) is required to meet the core requirements.
Trust and Data Boundaries
Trust boundaries separate what the Review App can access from what must remain protected:
- Config vars: All non‑secret config vars are copied. Secrets (e.g., API keys, database passwords) should be excluded from forks by marking them as
SECRETin the parent app’s settings; otherwise they would be duplicated into each Review App, violating confidentiality. - Add‑ons: If the Heroku Postgres add‑on is enabled, each Review App gets its own database instance. The parent app’s production database is never accessed directly.
- External services: Any third‑party API called by the app must be configured to accept traffic from the Review App subdomain or be mocked/stubbed. Otherwise, test traffic could affect production‑level services or incur charges.
- Network: Review Apps are isolated by dyno; they cannot reach other apps’ dynos unless explicitly exposed via public endpoints.
Operational Checks
To confirm the design works as intended, perform the following checks:
- Slug size verification: After a build, run
heroku slugs:info -aand ensure the size is < 500 MB. If larger, consider removing large assets or using.slugignore. - Dyno formation: With the Review App running, execute
heroku ps -a -to see the expected dyno types (e.g.,web.1). - Add‑on attachment: Run
heroku addons -a -to confirm the Postgres add‑on appears (if enabled). - Subdomain uniqueness: Verify the app URL follows the pattern
-.herokuapp.comin the Heroku Dashboard or viaheroku apps:info -a -. - Lifecycle cleanup: Close or merge the PR, then run
heroku apps:info -a -; the command should return "App not found" indicating destruction. - Dyno usage monitoring: Before opening a PR, note the dyno hours used in Heroku Metrics. After the Review App is destroyed, ensure the usage returns to the baseline, confirming no orphaned dynos remain.
Failure Modes
- Build failure: If the slug compilation exceeds 500 MB or a buildpack is incompatible, the Review App never starts. The platform logs the error; operators must fix the slug size or adjust buildpacks.
- Dyno quota exhaustion: Heavy PR traffic can consume the parent app’s dyno pool, causing throttling or preventing new Review Apps from launching. Mitigation: set a max concurrent Review Apps limit via the Heroku Dashboard or use a paid dyno plan.
- Postgres row limit: The hobby‑dev plan has a 10 000‑row limit. If a Review App’s migrations or test data exceed this, queries fail with "relation does not exist" or row‑limit errors. Solution: upgrade the add‑on plan for Review Apps or seed smaller datasets.
- Secret leakage: Forgetting to mark a config var as
SECRETduplicates it into every Review App, exposing it to anyone with access to the PR URL. Regular audits of config var flags prevent this.
Conditions That Would Change the Design
Re‑evaluate the architecture when any of the following occur:
- The parent app requires a add‑on that does not support per‑app branching (e.g., a fixed‑cost external service). In that case, introduce a shared staging service with environment‑specific credentials or use feature flags.
- Review App usage consistently exceeds the free dyno quota, making cost prediction difficult. Switch to a paid dyno tier or impose a hard limit on concurrent PR previews.
- Data isolation becomes insufficient because the app relies on mutable external state (e.g., a shared file store). Introduce per‑PR namespacing or mock the external service.
- Security policies forbid duplicating any config vars, even non‑secret ones, to forked environments. Then adopt a central configuration service (e.g., HashiCorp Vault) and inject only required secrets at runtime.
Practical Verification Example
Below is a minimal end‑to‑end checklist you can run in a terminal. Replace placeholders with your actual values.
# 1. Ensure you have Heroku CLI and are logged in
heroku login
# 2. Create or identify the parent app (must already be linked to a GitHub repo)
heroku apps:create parent-demo-app # optional, if not existing
# 3. Enable Review Apps in the Dashboard (or via CLI)
heroku integrations:github --app parent-demo-app --repo yourorg/yourrepo
# 4. Open a PR with a trivial change (e.g., update README)
git checkout -b docs/fix-typo
# edit a file, commit, push
git push origin docs/fix-typo
# then open the PR on GitHub
# 5. Wait for the Review App to appear; verify its name
heroku apps:parent-demo-app-123 # where 123 is the PR number
# 6. Check slug size
heroku slugs:info -a parent-demo-app-123
# 7. Check dyno formation
heroku ps -a parent-demo-app-123
# 8. Check Postgres add‑on (if enabled)
heroku addons -a parent-demo-app-123
# 9. Close the PR on GitHub
#10. Confirm destruction
heroku apps:info -a parent-demo-app-123 # should error "App not found"
#11. Verify no lingering dyno usage (optional)
# Open Heroku Metrics → Dyno usage before and after; values should match.
This sequence demonstrates the smallest design, the trust boundary (no secrets copied unless explicitly allowed), and the operational checks that confirm proper creation and cleanup. Adjust the commands for your specific stack, but keep the verification steps to ensure no orphaned resources remain.
Note: The example commands illustrate the expected workflow; they are not guaranteed to succeed in every environment. Always test in a non‑production context and review Heroku’s current documentation for limits and pricing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.