The Civora host application. It is a Decidim 0.32.1 install that mounts the decidim-contracts_sk engine at /contracts (inside Decidim's locale scope, so the pages live at /sk/contracts; the old /contracts paths redirect permanently). The engine provides the Slovak public contracts workflow and catalogue. This repository adds the deployment, the Slovak-first shell and the operations tooling around it.
Issues for Civora are tracked in civora-org/civora-platform, not in this repository.
- No production deployment exists yet. Hosting for a municipality is provisioned when a pilot is close. Until then the stack runs locally in Docker.
- A public demo is documented in
docs/ops/demo-server.md. It uses fictional data only and is wiped regularly. Nothing real may be put on it. - The engine is pinned to a release tag, v1.8.0. The engine release notes are in its CHANGELOG.
| Component | Version |
|---|---|
| Decidim | 0.32.1 (Rails 8.1) |
decidim-contracts_sk |
v1.8.0 (pinned in the Gemfile) |
| Ruby | 3.4.7 (.ruby-version) |
| libvips | system library for image processing (replaces ImageMagick in Decidim 0.32); libvips42t64 in the image |
| Node.js | 22.14.0 (.node-version, for Shakapacker) |
| PostgreSQL | 17 (Compose and CI) |
| Redis | 7 (Compose; required by the production Action Cable config) |
- Slovak-first shell: contracts-only header and footer, the organisation's colours and logo, and a Slovak locale fix. See Customisations.
- Participation links: contracts can be linked to Accountability results and Budgets projects, and the linked contracts appear on those pages.
- Admin second factor: TOTP for admins and engine role holders, wired into the engine's second-factor seam. See
docs/ops/two-factor.md. - Nightly CRZ sync: a scheduler service imports the organisation's CRZ mirrors. See
docs/ops/crz-sync.md. - Accessibility statement: a Slovak and English static page with a footer link. See
docs/ops/accessibility-statement.md. - Operations tooling: backups, a backup-gated deploy, restore drills, observability and secrets handling. See Operations.
compose.yaml defines the base stack:
| Service | Image | Purpose |
|---|---|---|
app |
built from Dockerfile (ruby:3.4.7-slim, non-root user) |
Rails and Decidim on port 3000. The entrypoint runs db:prepare and db:seed before starting the Rails server |
scheduler |
same image | Nightly CRZ sync loop (03:30 Europe/Bratislava). Runs scripts/crz_sync_scheduler.sh |
db |
postgres:17 |
Production database |
redis |
redis:7-alpine |
Action Cable |
Overlays are selected with COMPOSE_FILE in .env, or with -f:
| File | Purpose |
|---|---|
compose.dev.yml |
Bind-mount development loop: docker compose -f compose.yaml -f compose.dev.yml up |
compose.tls.yml |
Caddy in front of the app with automatic Let's Encrypt certificates, DECIDIM_FORCE_SSL=1, HSTS. Requires SITE_ADDRESS and ACME_EMAIL (tls.md) |
compose.observability.yml |
Self-hosted error tracking (GlitchTip), metrics (Prometheus), alerting (Alertmanager) and blackbox probes. See docs/ops/observability.md |
compose.demo.yml |
Public demo overlay (DEMO_MODE=1, no CRZ, mail written to files). Used with compose.tls.yml |
compose.drill.yml |
Throwaway overlay for scripts/restore_drill.sh |
Without Docker, using a host Ruby and a local PostgreSQL:
bundle install
DISABLE_SPRING=1 bin/rails db:create db:migrate db:seed
# one-time: gitleaks pre-commit hook (requires gitleaks, e.g. `brew install gitleaks`)
scripts/install-hooks.sh
bin/dev # Rails on 0.0.0.0:3000 and the Shakapacker dev server (Procfile.dev)Database settings come from the environment (config/database.yml): DATABASE_HOST (default localhost), DATABASE_PORT (5432), DATABASE_USERNAME and DATABASE_PASSWORD (blank means socket or peer authentication), and DATABASE_URL in production.
Prefix host-app rails and rake commands with DISABLE_SPRING=1. The Spring daemon otherwise keeps long-running sessions alive.
Smoke checks once the app runs:
| Path | Expected |
|---|---|
GET /up |
Rails liveness check (200) |
GET /healthz |
Database and Redis check. Returns 503 with a JSON status on failure |
GET /contracts |
Public catalogue of the engine |
Requires Docker with Compose v2 and network access to GitHub. The engine is fetched from its public repository at build time.
cp .env.example .env
# set SECRET_KEY_BASE (openssl rand -hex 64) and POSTGRES_PASSWORD (openssl rand -hex 16)
# leave GITHUB_TOKEN empty unless you build a private fork
docker compose up -d --build
curl -fsSL http://localhost:3000/contracts # expect 200 (-L: /contracts redirects to /<locale>/contracts)A GitHub token is passed as a BuildKit secret mount, never as a build argument, so it does not appear in the image history or layers.
A clean-state smoke test runs the whole sequence (down -v, build, wait for healthy, GET /contracts):
scripts/smoke_test.shCopy .env.example to .env. Never commit .env. The full variable inventory and the rotation procedure are in docs/ops/secrets.md.
| Variable | Required | Purpose |
|---|---|---|
SECRET_KEY_BASE |
yes | Rails secret. Rotating it invalidates encrypted data, so every TOTP enrolment must be reset with two_factor:reset_all (see two-factor) |
POSTGRES_PASSWORD |
yes | Password of the postgres user in the Compose database |
DECIDIM_ORG_HOST |
before the first pilot boot | Host name the seeded organisation answers on (default localhost) |
GITHUB_TOKEN |
private forks only | Read access to a private engine fork during the build |
SMTP_* |
at the first real deploy | Production mail. Not configured yet |
CRZ_ICO_ORG_<organization id> |
to run the CRZ import | The organisation's IČO. Without it the import refuses to run |
CRZ_SYNC_INITIAL_SINCE |
optional | First-run cursor for the CRZ sync (default: 7 days ago) |
CRZ_SYNC_PUSHGATEWAY_URL |
optional | Dead-man push target for the sync (default: the in-network pushgateway) |
ACTOR_EMAIL |
optional | Pins the admin account used as audit actor by the CRZ sync (default: the first admin who accepted the terms) |
GLITCHTIP_SECRET_KEY, GLITCHTIP_DB_PASSWORD |
with the observability overlay | Required by the GlitchTip services |
SENTRY_DSN, GLITCHTIP_DOMAIN, PUSHGATEWAY_URL |
optional | Error tracking and metrics wiring |
ALERT_* |
with the observability overlay | Alert delivery: Telegram for critical and high alerts, e-mail for all |
SITE_ADDRESS, ACME_EMAIL |
with compose.tls.yml |
Public host name and Let's Encrypt contact |
compose.yaml keeps DECIDIM_FORCE_SSL: "0" and publishes plain-HTTP port 3000 for local use. Anything public adds compose.tls.yml, which puts Caddy in front, forces HTTPS, sends HSTS and unpublishes port 3000 (docs/ops/tls.md).
The Gemfile pins the engine to a tag:
gem "decidim-contracts_sk", github: "civora-org/decidim-contracts_sk", tag: "v1.8.0"To work on a local engine checkout, use a Bundler local override. Never commit it:
bundle config set --local local.decidim-contracts_sk /path/to/decidim-contracts_sk
git -C /path/to/decidim-contracts_sk checkout v1.8.0 # Bundler refuses any other revision
# when done
bundle config unset --local local.decidim-contracts_skInside Docker, use a personal, untracked docker-compose.override.yml that mounts the engine checkout read-only at /opt/decidim-contracts_sk. Compose loads that file automatically, so pass -f compose.yaml explicitly when you want to run the built image.
- Cut an engine release and bump
tag:in theGemfile. - Run
bundle lock --update decidim-contracts_sk --conservative, review the lock diff and runbundler-audit. - Copy each new engine migration into
db/migrate/verbatim, with its original timestamp and the.decidim_contracts_sk.rbsuffix. Take it from the tag:git -C ../decidim-contracts_sk show v1.8.0:db/migrate/<file>.rb > db/migrate/<file-without-.rb>.decidim_contracts_sk.rb
- Do not run
bin/rails decidim_contracts_sk:install:migrationshere. It re-stamps timestamps, and a database that already ran the engine migrations would run them again. - Migrate, commit the regenerated
db/schema.rb(its diff should contain only the engine's tables, columns and the version), rebuild the image and walk through the demo test plan.
Check the engine's CHANGELOG.md and its docs/ for host follow-ups for each release.
.github/workflows/ci.yml runs on every pull request and on pushes to main:
- RuboCop;
- Minitest against PostgreSQL 17;
- Brakeman with
--exit-on-warnand one documented ignore for the upstream-pinned end-of-life Rails warning (config/brakeman.ignore); bundler-audit, with per-advisory ignores that are blocked upstream, each with a revisit condition (.bundler-audit.yml);- a full-history gitleaks scan (
.gitleaks.toml).
The workflow still uses an ENGINE_READ_TOKEN secret to check out the engine. The engine repository is public, so that token is no longer needed and the checkout steps can be removed.
The hosting model is one stack per city (ADR-011). Onboarding is scripted and idempotent, keyed by the organisation id. See docs/ops/onboard-municipality.md for the full procedure, including the steps that stay manual (DNS, TLS and the people).
bin/rails "civora:onboard:organization[config/onboarding/<city>.yml]"
bin/rails "civora:onboard:crz_sync[<organization_id>,<SINCE ISO8601>]" # rate-limit aware, resumable
bin/rails "civora:onboard:verify[<organization_id>]"config/onboarding/example.yml is the template. It uses example.org addresses only.
After onboarding, the pilot checklist in short:
- Give the stack its own host name and HTTPS: DNS, then
compose.tls.ymlwithSITE_ADDRESS,ACME_EMAILand a matchingDECIDIM_ORG_HOST(docs/ops/tls.md). - Create a fresh database on the pilot host. A pilot is never a copy of the demo database.
- Create the system admin, then set the default locale to
skand the organisation's logo and colours in/system. - Invite the municipality's admins. Each one enrols a second factor before using the admin. Check with
bin/rails two_factor:status. - Make sure the pilot has at least two people with engine roles. The person who submits a contract cannot approve, return or reject it. A single-person municipality must opt out explicitly with
Decidim::ContractsSk.allow_self_review = trueinconfig/initializers/contracts_sk.rb. Self-reviews are then audited. - Schedule the CRZ sync, the search-alert digest (
decidim_contracts_sk:subscriptions:deliver, daily) and the backups withBACKUP_STRICT=1(age key pair generated and stored, bucket at another provider,PUSHGATEWAY_URLset;restore-runbook.md), and log one restore drill from the off-site copy on a second machine. - Walk the demo test plan on the pilot stack with
MANUAL-2026-records, then archive them.
- Demo data:
bin/rails "decidim_contracts_sk:seed_demo[<organization_id>]"seeds fictional contracts in every lifecycle state. It is idempotent. Never run it on a pilot database. - Demo admins:
contracts-admin@example.organdcontracts-editor@example.org. The seed sets random passwords, stored in the git-ignoredtmp/demo-admin-credentials.txt. Under the four-eyes rule, submit as the editor and decide as the admin. - Test plan:
docs/qa/demo-test-plan.mdis the click-through run for each release. Test records use theMANUAL-2026-prefix and are archived after each run, because the demo database also supplies screenshots for civora-org/civora-site.
The engine provides markup only. The Civora look and content live in this repository:
| Area | Where |
|---|---|
| Colours, logo and print layer | docs/appearance.md, app/packs/stylesheets/decidim/decidim_application.scss |
| Header: Help and Zmluvy (the catalogue) instead of Meetings and Activity; a Participácia link when the town has published processes | app/views/layouts/decidim/header/_main_links_desktop.html.erb, app/lib/participation_navigation.rb |
| Homepage hero: a second button to the processes when they exist | app/cells/decidim/content_blocks/hero/cta_button.erb |
Topbar search: submits to the catalogue's q filter |
app/views/layouts/decidim/header/_main_search.html.erb |
| Footer: Resources holds Open Data only | app/views/layouts/decidim/footer/_main_links.html.erb |
| Slovak locale fix and footer copy | config/locales/sk.yml |
| Homepage hero, en and sk, idempotent | db/seeds.rb |
| Participation links: resolver and two view overrides | app/lib/participation_link_resolver.rb, app/views/decidim/accountability/results/_result.html.erb, app/views/decidim/budgets/projects/show.html.erb |
| Admin second factor | app/lib/two_factor, app/controllers/two_factor, config/initializers/two_factor.rb |
| Engine configuration seams | config/initializers/contracts_sk.rb |
The view overrides replace decidim-core, decidim-accountability and decidim-budgets 0.32.1 partials by path. Re-check them on every Decidim upgrade. A test fails when the two participation-link views, or the header links partial, drift from the installed gems.
Participation link setup: docs/ops/participation-link.md.
| Document | Covers |
|---|---|
docs/ops/secrets.md |
Secrets policy, inventory, gitleaks guardrails, rotation |
docs/ops/environments.md |
Development, test, staging and production definitions. Staging is defined but not provisioned |
docs/ops/onboard-municipality.md |
Scripted municipality onboarding |
docs/ops/demo-server.md |
Public demo server: provisioning, deploy, nightly reset, teardown |
docs/ops/tls.md |
TLS terminator (Caddy), forced HTTPS, HSTS, forwarded headers, local rehearsal |
docs/ops/demo-proposal-to-contract.md |
Demo seed for a proposal, budget project, result and linked contract |
docs/ops/crz-sync.md |
Nightly CRZ sync scheduler |
docs/ops/two-factor.md |
Admin TOTP: who must enrol, sign-in flow, lockout, operator reset |
docs/ops/participation-link.md |
Contract links to results and projects |
docs/ops/participation-navigation.md |
Header link, hero button and the homepage processes block (civora:home:participation) |
docs/ops/accessibility-statement.md |
Accessibility statement and its publishing task |
docs/ops/restore-runbook.md |
Encrypted off-site backups, retention, restore, deploy gate, DPA mapping, local rehearsal |
docs/ops/restore-drill-log.md |
Log of executed restore drills |
docs/ops/decidim-032-secrets-runbook.md |
Decidim 0.32 secret-key and encryption upgrade: backups first, upgrade sequence, rollback, restore drill (not yet run) |
docs/ops/log-policy.md |
Where logs live, rotation, levels, privacy rules |
docs/ops/observability.md |
Error tracking, metrics, alerting, backup dead-man switch |
docs/ops/incident-response.md |
Roles, severity, response targets, breach notification (sk templates) |
docs/ops/security-updates.md |
Patch cadence, emergency patches, review of upstream-blocked advisories |
| Script | Purpose |
|---|---|
scripts/backup.sh |
pg_dump and attachments archive encrypted with age, manifest and checksums in backups/, the dead-man ping, then the off-site copy |
scripts/backup_offsite.sh, scripts/backup_fetch.sh, scripts/backup_decrypt.sh |
Upload and verify a backup in an S3-compatible bucket; download one; decrypt one on the operator's machine |
scripts/backup_prune.sh |
Retention: 30 days of daily backups, local and off-site |
scripts/deploy.sh |
Backup-gated deploy: backup, verify, up, smoke test, monitoring review. Deploys must use it |
scripts/restore_drill.sh |
Restores (--offsite: from the bucket, decrypting with the operator's age key) into an isolated stack, verifies and logs the result |
scripts/smoke_test.sh |
Clean-state build and smoke test |
scripts/install-hooks.sh |
Installs the gitleaks pre-commit hook |
scripts/crz_sync_run.sh, scripts/crz_sync_scheduler.sh |
Run one CRZ sync pass, and the scheduler loop used by the scheduler service |
scripts/deploy.sh aborts unless a verified backup has been taken first. It also re-checks /healthz after the smoke test.
| Task | Purpose |
|---|---|
civora:onboard:organization[<config>], civora:onboard:crz_sync[...], civora:onboard:verify[...] |
Municipality onboarding |
civora:accessibility_statement:publish[<organization_id>] |
Publish the accessibility statement (idempotent) |
civora:demo:proposal_to_contract[<organization_id>], civora:demo:prepare_server[...] |
Demo seeds |
two_factor:status, two_factor:reset, two_factor:reset_all |
List second-factor enrolment; reset one or all users (see two-factor) |
decidim_contracts_sk:* |
Engine tasks: CRZ import, search-alert delivery, demo seed. See the engine README |
Backups run from scripts/backup.sh: encrypted with age (the private key is never on the server), copied to an S3-compatible bucket at another provider, kept 30 days and pruned by scripts/backup_prune.sh; a missed backup or a missed off-site copy raises an alert. Restore procedures, key custody and drills are in docs/ops/restore-runbook.md and docs/ops/restore-drill-log.md; key custody is in docs/ops/secrets.md. Secrets come from environment variables only. Rails credentials are retired.
- No production deployment exists yet. The stack has not run with real municipal data.
- Migrations run on boot (
db:prepare). That is acceptable for one instance, not for replicated setups. - The image is built for the host architecture only. Multi-architecture builds are a follow-up.
- Builds fetch the engine from GitHub, so they need network access. The tag pin keeps the result deterministic.
- The staging environment is defined but not provisioned.
AGPL-3.0, the same licence as Decidim. See LICENSE-AGPLv3.txt.