Sync to Heroku
Heroku hands an app's config vars to every dyno as environment variables when it
starts them, so there is no earlier point at which seekrit could inject them. A
binding owns one Heroku app's config vars — the same slot heroku config:set
writes.
| At a glance | |
|---|---|
| What seekrit writes | An app's config vars, delivered to every dyno and process type |
| Addressed by | The Heroku app name, or its UUID |
| Connection carries | The token alone — Heroku app names are globally unique |
| Token permission | A token whose user has the operate or deploy role on the app |
| Takes effect | Immediately — a new release, and the dynos restart |
| Value visibility | Readable by anyone with access to the app's config |
New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.
1. Create an API token
heroku authorizations:create --short
Reach for that rather than heroku auth:token: the CLI's own token expires a
year after you log in — or eight hours if your account uses SSO — and a sync
connection built on it stops working overnight, long after you have forgotten
where it came from. authorizations:create mints a token that does not expire.
The token carries its user's access to every app and team they can reach, and Heroku app names are globally unique, so a Heroku connection has nothing else to configure:
printf '%s' "$(heroku authorizations:create --short)" \
| seekrit sync connect --name acme-heroku --provider heroku
A Heroku token is only as narrow as the user who made it. There is no per-app API token the way Fly has one, so the least-privilege setup is a machine user added to just the apps it syncs, with the deploy or operate role.
2. Bind an environment
A Heroku app has one set of config vars, shared by every dyno and every process type, so the app is the whole destination — there are no targets to pick. Heroku's own convention is that staging and production are separate apps, and that is the split a binding maps onto:
seekrit sync verify acme-heroku --provider heroku --heroku-app storefront-production
seekrit sync enable --connection acme-heroku --provider heroku \
--heroku-app storefront-production \
--app storefront --env production --acknowledge-decryption
--heroku-app is Heroku's app; --app is the seekrit application the
environment belongs to. They are different things, which is why the flag is not
just --app.
You can pass the app's UUID instead of its name, and that is the sturdier choice
— renaming an app in the Heroku dashboard breaks a binding that holds its name.
seekrit checks a name against Heroku's own pattern before storing it, so the
habitual slips (pasting example.herokuapp.com, or a name with capitals) fail at
the form rather than as a 404 inside a run nobody is watching.
Verify fetches the app rather than reading its config vars: that proves the token and that the app exists — every failure you can cause from the connection dialog — without pulling a single value into seekrit.
Every push restarts the app. Heroku applies config vars by cutting a new
release and restarting the dynos — the same thing heroku config:set does. A
sync run that writes anything will do that to storefront-production.
seekrit keeps it to one release per run: a run's sets and removals travel
in a single request, and a run with nothing to write sends no request at all.
But if brief restarts are costly for this app, bind it in manual mode
(--mode manual) and push with seekrit sync run when you choose, rather than
on every write.
How a push behaves
- Exactly one request per run.
PATCH /apps/{app}/config-varscarries the sets and the removals together in one flat map. No other connector does that — here a request is the expensive unit, because each one bounces the dynos. - Merge semantics. A name with a value is set, a name with
nullis removed, and a name that is absent is left alone. - Add-on credentials survive. That matters more here than anywhere else,
because Heroku apps are full of config vars that are not yours: add-ons write
DATABASE_URL,REDIS_URL, and their kin, and rotate those values on their own schedule. seekrit never sends a name it was not given, so add-on credentials,HEROKU_*, and anything you set by hand survive a push untouched. - A rejected request is blamed on every name in it, which is honest here:
Heroku validates the whole map before applying it, so a rejected
PATCHleaves the app exactly as it was and none of those names landed. - Invalid names are failed without being sent. Because the whole run is one request, a single unacceptable name would otherwise take the entire environment down with it. seekrit checks names against Heroku's rules first and records the offenders as per-name failures, leaving the rest to push normally.
- A value Heroku echoes back in an error is scrubbed before the reason is stored on the run row.
Name and value rules
Heroku will not accept every name seekrit will. Config var names take only
letters, numbers, and underscores, cannot begin with a digit or a double
underscore, and cannot begin with HEROKU_, which Heroku reserves for itself.
A name that breaks one of those rules is reported as a failure against that
name alone — the rest of the environment still pushes — so watch for it if you
use a --prefix.
Heroku also caps an app's config vars at 64KB across every key and value. seekrit checks its own payload against that before sending, but the app may already hold config vars seekrit does not manage, so passing that check is not a promise Heroku will accept it — only failing it is a certainty that Heroku would not.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 on every name | The token expired or was revoked — likely heroku auth:token, which expires (8 hours under SSO) | Re-create the connection with heroku authorizations:create --short |
403 | The token's user lacks the deploy or operate role on this app | Add the machine user to the app with the right role |
404 on the app | Wrong name, a .herokuapp.com URL, or the app was renamed | Bind by UUID instead — a rename then can't break it |
| One name failed, rest landed | The mapped name breaks Heroku's rules — a digit or HEROKU_ at the front, a stray character | Fix the prefix or rename; seekrit refuses to send it rather than fail the run |
422 about the app total | The app's config vars exceed 64KB across all keys and values | Trim, or move bulk config out of env vars |
| Every name reported failed at once | The single PATCH was rejected | Nothing landed — Heroku validates before applying. Fix the cause and re-run |
| Dynos restarting more than you'd like | Every write cuts a release | Bind with --mode manual and push with seekrit sync run |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- CLI reference — every
seekrit syncflag