Appearance
Deploying a change
bin/docker-entrypoint runs db:prepare when the server boots. That migrates, and it seeds only a database it has just created.
Everything on this page follows from that one sentence.
Permissions added by a branch
A permission that lives only in db/seeds.rb will not exist on any environment whose database already exists. Nobody holds it, every policy asking for it answers false, and the effect is not an error — it is a missing menu.
How this actually presented
Settings → Contract templates renders only when its policy passes, which needs signing_template.view. With no such row the item is simply absent, and the feature reads as a deploy that did not take. It was reported from staging as "I can't see the contract feature".
Diffing the permission list against master — rather than waiting for each one to be reported — found six in that state. Between them they hid three settings screens and screening adjudication.
So: if your branch adds a permission, add a migration that creates and grants it. db/seeds.rb stays the source of truth for a fresh install and must list the same grants; the migration is what reaches everywhere else.
db/migrate/20260901090000_create_the_permissions_seeds_alone_would_never_reach.rb is the pattern.
Checking before you deploy
bash
bin/rails runner '
creators = Dir["db/migrate/*.rb"].select { |f| File.read(f) =~ /Permission\.(find_or_initialize_by|find_or_create_by|create)/ }
created = creators.flat_map { |f| File.read(f).scan(/"([a-z_]+\.[a-z_.]+)"/).flatten }.uniq
seeded = File.read("db/seeds.rb")[/^permissions = \{(.*?)^\}\.freeze/m, 1].scan(/"([a-z_.]+)" =>/).flatten
puts (seeded - created).inspect
'Anything it prints exists only in seeds. That is not automatically wrong — most of those predate this rule and are already on every environment — but anything your branch added needs a migration.
Seeds refuse to revoke
db/seeds.rb is additive. If a role holds a permission the seed does not list, seeding halts rather than removing it — a drifting list must not silently strip somebody's access.
The consequence is that an accidental grant is permanent until a migration removes it, and it blocks every later seed step until then. Two roles once acquired signing_template.manage from a seed run against an intermediate list, and that halted seeding on every environment afterwards.
To remove a grant deliberately, write a migration. See 20260819101000_stop_the_mlro_authoring_contract_wording.rb.
Data migrations that touch live access
A migration that revokes a permission changes what real people can do, on a shared environment, at deploy time. That is legitimate — but say so in the pull request, because the first anyone notices otherwise is a colleague who has lost a screen.
Countries
Activating a country creates a CountryConfiguration, and nothing in the UI can create one — the country-defaults screen lists and edits, it does not add. The development bootstrap that creates them runs in development only.
bash
bin/rails countries:activate COUNTRY=BH ACCOUNT_ID=…
bin/rails countries:listContract templates
Template wording is data, and the export file is meant to be committed so it travels with the repository:
bash
bin/rails signing:import FILE=db/contract_templates/saudi.json ACCOUNT_ID=…The import recomputes the content digest before writing: byte-identical wording or nothing.
HTTPS
FORCE_SSL=true turns on HSTS, secure cookies, and https link generation behind a terminating proxy. It defaults to off, and enabling it is a deliberate act per environment.
Verify the header first
Turning it on where the proxy does not forward X-Forwarded-Proto produces a redirect loop whose first casualty is the health check — which reads as a bad deploy rather than as a setting.
Deploy with it off, prepare a contract, and check the signing link on the page begins https://. If it does, enable it. /up is excluded from the redirect so the health check survives either way.
This matters more than it used to: a signing link is a bearer credential in a URL that an external signatory opens.
What CI refuses
A pull request must pass four jobs: lint (RuboCop), scan_js, scan_ruby and test. scan_ruby runs two things — Brakeman over the code, and bundler-audit over the gems.
bundler-audit checks against advisories published today, so a branch that has not changed can go red overnight. Run it with a fresh database before assuming a failure is yours:
bash
bundle exec bundler-audit check --updateWithout --update it reads the local copy of the advisory database, which can be weeks old and will happily report nothing.
Brakeman fails on any warning, including a weak-confidence one. That is deliberate — a warning waved through once is a warning nobody reads again — so a false positive is fixed in the code rather than added to an ignore file. The usual case is join(...).html_safe on pieces that are already escaped: safe_join says the same thing without asserting it.
CI's Postgres requires a password and CI sets a URL per database (DATABASE_URL, CACHE_DATABASE_URL, QUEUE_DATABASE_URL, CABLE_DATABASE_URL). Anything that works only because a local Postgres trusts local users will fail there — see tests that pass for the wrong reason.
After deploying
- The Contracts card appears on a merchant without anyone running
db:seed - A signing link begins
https:// bin/rails countries:listshows the countries you expect