Vercel-to-VPS environment synchronization
Section titled “Vercel-to-VPS environment synchronization”NutsNews keeps one source of truth with two deployment targets:
Vercel Production variables --reviewed mapping--> VPS production app envVPS-only variables ---------protected infra input--> VPS production app envVercel is the source only for variables explicitly enabled in
ramideltoro/nutsnews-infra/config/vercel-vps-env-sync.json. The VPS is never
written directly over SSH. The protected Ansible workflow renders
/etc/nutsnews/nutsnews-app.env with mode 0600; Ansible is the only writer.
Variable classification
Section titled “Variable classification”The mapping is an allowlist, not a copy-everything rule. A Vercel Production variable that is absent from the mapping fails the sync. A variable classified for manual review also stops the sync.
| Category | Current policy | Examples |
|---|---|---|
| Safe to synchronize | Explicitly allowlisted; values are public or non-credential runtime configuration used by the web app. | ADMIN_EMAILS, ADMIN_SHARD_*, legacy Vercel NEXT_PUBLIC_* sources that map to NUTSNEWS_PUBLIC_* runtime destinations, NUTSNEWS_EDGE_FEED_SNAPSHOT_URL, and the non-secret NUTSNEWS_* runtime/data/project identity variables |
| Server-side secrets to synchronize securely | Supported only through an explicit sync: true mapping and the protected Ansible secret path. | ACTIONS_READ_TOKEN, AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, AUTH_SECRET, BETTER_STACK_SOURCE_TOKEN, HOME_SERVER_STATS_API_KEY, RESEND_API_KEY, SUPABASE_SERVICE_ROLE_KEY, and TURNSTILE_SECRET_KEY are synchronized because the current VPS web runtime consumes them. |
| Vercel/platform-only | Excluded. | VERCEL_*, NOW_*, NEXT_PUBLIC_VERCEL_*, deployment commit metadata, SENTRY_ORG, SENTRY_PROJECT, and unused cost-estimation metadata |
| Preview/development-only | Excluded from the production sync. | Names containing the mapped preview/development markers; non-production Vercel targets are never fetched as the source. |
| Manual review | Fails closed until a consumer, target, and security classification are documented in the mapping. | Any newly discovered Turnstile, admin, controller, or other service-specific variable not yet represented by an exact rule |
Runtime safety identity variables
Section titled “Runtime safety identity variables”The VPS needs the same explicit production runtime policy as Vercel because the web image validates it at startup and readiness. The reviewed mapping synchronizes these non-secret values only:
NUTSNEWS_RUNTIME_ENV=productionNUTSNEWS_SIDE_EFFECTS_MODE=liveNUTSNEWS_DATA_ENVIRONMENT=productionNUTSNEWS_SUPABASE_CREDENTIALS_ENV=productionNUTSNEWS_SUPABASE_PROJECT_REF=<production project identity>NUTSNEWS_PRODUCTION_SUPABASE_PROJECT_REF=<production project identity>NEXT_PUBLIC_NUTSNEWS_RUNTIME_ENV=productionNEXT_PUBLIC_NUTSNEWS_SIDE_EFFECTS_MODE=liveThe protected workflow fails closed if a Vercel Production variable is unclassified. Add an exact reviewed mapping and regression test before changing the VPS environment; never edit /etc/nutsnews/nutsnews-app.env manually.
Automatic release compatibility
Section titled “Automatic release compatibility”Simple Summary
Section titled “Simple Summary”When a checked NutsNews change reaches the app main branch, the release system can move the same checked image to the VPS. The required production safety label is now recognized, so the release does not stop just because it sees that label.
Intermediate Summary
Section titled “Intermediate Summary”The automatic release path remains deliberately strict: an app release sends an immutable digest to the infrastructure repository, which reviews the release manifest through automation and starts Protected Ansible Apply. Vercel Production variables are synchronized only when each name has an explicit rule. NUTSNEWS_DATA_ENVIRONMENT is now one such non-secret runtime-safety identity. NUTSNEWS_DATA_ENV remains a legacy compatibility input, but it does not replace the required NUTSNEWS_DATA_ENVIRONMENT policy field.
Expert Summary
Section titled “Expert Summary”The protected sync fails before Ansible when it finds an unclassified Vercel Production variable. This protects against silent configuration expansion, but it means runtime-contract changes must update the reviewed mapping and its tests atomically. The exact NUTSNEWS_DATA_ENVIRONMENT rule renders the same named VPS runtime field and is covered by both the production-inventory and guardrail tests. The value remains private to the protected environment flow: reporting includes only names and fingerprints, never values. Unknown names and manual-review classifications continue to block the release.
Release retries first fetch current infrastructure main and verify the reviewed manifest against the requested immutable digest. If it already matches, the retry skips duplicate release-PR creation and proceeds to the protected apply. If a promotion is still needed, its branch starts from the fetched origin/main rather than the historical workflow checkout. This keeps retries idempotent when an infra fix merges between an initial failed dispatch and its rerun.
After dispatching Protected Ansible Apply, the release workflow selects only a run created at or after that dispatch time. This prevents an older failed apply with the same release title from being mistaken for the newly dispatched run.
flowchart LR app[App main release] --> image[Published immutable GHCR digest] image --> dispatch[Repository dispatch to infra] dispatch --> mapping{All Vercel Production variables classified?} mapping -- no --> stop[Fail closed before Ansible] mapping -- yes --> manifest[Checked VPS digest manifest] manifest --> apply[Protected Ansible Apply] apply --> verify[Digest and health verification]Rollback
Section titled “Rollback”If a variable is newly unclassified, do not bypass the check or edit the VPS environment manually. Add a reviewed exact mapping and regression test, merge through the normal infrastructure PR flow, then rerun the failed release. To remove the field later, remove it from Vercel Production only after a reviewed mapping and runtime-policy change has established a safe replacement.
SENTRY_AUTH_TOKEN is classified as a server-side secret but excluded because it
is a Vercel build/source-map credential. OPENAI_API_KEY remains excluded
because it belongs to the Worker runtime, while cost-estimation variables remain
excluded because the VPS web runtime does not consume them. Browser-readable
values are not secrets by virtue of their name; only the reviewed public
configuration above is copied.
The current web runtime has two Supabase consumers: browser code receives
NUTSNEWS_PUBLIC_SUPABASE_URL and NUTSNEWS_PUBLIC_SUPABASE_ANON_KEY only
through the no-store runtime configuration endpoint, while server-side admin
dashboards require SUPABASE_SERVICE_ROLE_KEY and accept SUPABASE_URL or the
runtime public URL fallback. During the migration, the mapping reads the
existing Vercel NEXT_PUBLIC_SUPABASE_* source values but renders
NUTSNEWS_PUBLIC_SUPABASE_* on the VPS. The service-role key is never exposed
through a browser-readable name or bundle.
The same destination rule applies to Sentry, analytics, Turnstile, and the iOS
app-store URL: existing Vercel NEXT_PUBLIC_* source values are deliberately
translated to NUTSNEWS_PUBLIC_* runtime destinations. Do not add those values
as Docker build arguments or render legacy NEXT_PUBLIC_* names into the VPS
app environment. See Dual-Target Web Deployment
for the exact runtime contract and rollback procedure.
Required credentials and identifiers
Section titled “Required credentials and identifiers”Store these in the production-vps GitHub Actions Environment for
ramideltoro/nutsnews-infra:
NUTSNEWS_VERCEL_TOKEN: a short-lived, least-privilege Vercel access token that can read and decrypt the owning team’s project environment variables. The token owner must have access to the project and its environment secrets. Set an expiration and rotate it before expiry. Store it only in theproduction-vpsGitHub Environment; do not put it in repository variables, files, command arguments, or workflow outputs.NUTSNEWS_VERCEL_PROJECT_ID: the Vercel project identifier.NUTSNEWS_VERCEL_TEAM_ID: the owning Vercel team identifier.
Do not store Vercel environment values in GitHub variables, workflow outputs,
artifacts, repository files, or logs. Existing VPS-only values remain in the
protected NUTSNEWS_APP_ENVS_JSON input. The sync overlays only the selected
Vercel keys in memory; it does not make the VPS secret map a second Vercel
source of truth.
Dry run / diff
Section titled “Dry run / diff”The workflow is serialized and runs behind the existing production-vps
Environment approval. From a machine authenticated to the repository, run:
gh workflow run "Protected Ansible Apply" \ --repo ramideltoro/nutsnews-infra \ -f run_mode=check \ -f confirm_apply= \ -f sync_vercel_production=true \ -f enable_cloudflare_ddns=false \ -f enable_grafana_alloy=falseApprove the production-vps Environment gate when prompted, then review the
workflow output. The Vercel API response is classified without printing values;
the diff lists only added, changed, removed, and excluded variable names.
The sync first reads Production metadata, then retrieves each selected value by
ID through Vercel’s per-variable decrypted-value endpoint. Ansible check mode
must finish with a healthy recap before any apply.
Approval and apply
Section titled “Approval and apply”After reviewing the dry-run, the exact production-changing command is:
gh workflow run "Protected Ansible Apply" \ --repo ramideltoro/nutsnews-infra \ -f run_mode=apply \ -f confirm_apply=vps.nutsnews.com \ -f sync_vercel_production=true \ -f enable_cloudflare_ddns=false \ -f enable_grafana_alloy=falseThis still requires approval at the existing production-vps GitHub Environment
boundary. Do not run the apply command until the check run is reviewed. The
workflow fetches Vercel Production again immediately before Ansible runs, so a
stale dry-run cannot silently apply an unrelated later value.
The workflow overlays the classified Vercel map onto the VPS app map before
passing Ansible extra vars. This is intentional: Ansible extra vars have higher
precedence than role facts, so merging only inside the role would not replace a
pre-existing NUTSNEWS_APP_ENVS_JSON map.
After an approved apply, perform the required read-only verification: confirm the env variable names and name-only fingerprints, confirm the app Compose service configuration, and check the configured health endpoint. Never print the env file or its values. A successful workflow without live verification is not a claim of VPS success.
Rollback and recovery
Section titled “Rollback and recovery”The source-controlled rollback is Vercel-first:
- Restore the last known-good value in the Vercel Production environment using the dashboard or a secure stdin-based Vercel CLI/API procedure. Do not put a secret in shell history or a command argument.
- Run the dry-run command above and verify that only the expected variable name is changed.
- Run the apply command through the same protected Environment approval.
- Re-run read-only health and service verification.
If Ansible fails, do not edit the VPS env file or retry apply blindly. Keep the
source unchanged, inspect the failed task and sanitized recap, correct the
mapping or source through review, run check mode again, and then apply. The
Ansible template task is no_log and the workflow removes temporary secret
files in an always cleanup step.
If the sync reports that a value is undecrypted or semantically invalid, do not
apply. A 403 while retrieving a selected secret means the protected Vercel
token lacks project/team permission to read environment-variable secrets. Fix
the token in the Vercel account/team token settings and update only
NUTSNEWS_VERCEL_TOKEN in the production-vps GitHub Environment. For an
invalid source value, repair the Vercel Production variable, rerun check mode,
and review the name-only diff. If an unusable value was already applied, restore
the last known-good Vercel Production value, run check mode, apply through the
same approval boundary, and perform read-only VPS health verification.
Rotate or remove a variable
Section titled “Rotate or remove a variable”To rotate a synchronized variable, update its value in Vercel Production, run
the dry-run, review the name-only change, and run the protected apply. To remove
one, remove it from the Vercel Production environment, run the dry-run, and
apply; the workflow reports the synchronized key as removed and Ansible
removes it from the rendered file. If the key must remain on the VPS, it is a
VPS-only variable and must be deliberately managed in the protected infra
input, not restored by bypassing the mapping.
To add a new key, first add an exact mapping rule with its category, destination,
reason, and explicit sync value in an infra PR. Do not broaden a pattern to
make an unknown variable pass. Update this document when the runtime consumer or
security classification changes.
Official Vercel behavior used
Section titled “Official Vercel behavior used”The workflow uses bearer-token authentication and the owning teamId with
Vercel’s documented GET /v10/projects/{idOrName}/env endpoint to inspect
Production targets and metadata. It then calls
GET /v1/projects/{idOrName}/env/{id} for each selected variable. The list
endpoint’s older decrypt=true query parameter is deprecated; its value
field is not treated as plaintext merely because that parameter was supplied.
The per-variable response must report a usable decrypted value before it can
enter the private temporary selection file.
The sync rejects missing values, undecrypted encrypted/secret/sensitive values,
encrypted-envelope-shaped strings, newlines, and invalid Auth.js/admin shapes.
AUTH_GOOGLE_ID must be a plausible Google Web client ID,
AUTH_GOOGLE_SECRET must be nonempty, AUTH_SECRET must be at least 32
characters, and ADMIN_EMAILS must be a comma-separated email list. Failure
messages identify variable names only.
