Skip to content

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:list

Contract 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 --update

Without --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:list shows the countries you expect