• Rust 47.8%
  • Vue 25.7%
  • TypeScript 24.8%
  • CSS 0.7%
  • JavaScript 0.5%
  • Other 0.4%
Find a file
Lucas Jahier 7c34dbb220
Some checks failed
CI / Style (push) Has been cancelled
CI / Test (push) Has been cancelled
docs: overhaul local setup and harden Documenso development configuration
2026-09-16 21:57:14 +02:00
.config chore: update ci 2025-12-22 03:53:13 +01:00
.github/workflows chore: update ci 2025-12-22 03:53:13 +01:00
app docs: overhaul local setup and harden Documenso development configuration 2026-09-16 21:57:14 +02:00
crates feat: add Documenso/CNIL tracking and fix encrypted document handling 2026-09-16 20:57:48 +02:00
migrations feat: add Documenso/CNIL tracking and fix encrypted document handling 2026-09-16 20:57:48 +02:00
scripts docs: overhaul local setup and harden Documenso development configuration 2026-09-16 21:57:14 +02:00
src feat: add db source age public keys for file encryption + support multiple keys 2026-03-18 15:19:05 +01:00
.dockerignore feat: admin + cleanup 2026-02-08 04:43:57 +00:00
.env.sample docs: overhaul local setup and harden Documenso development configuration 2026-09-16 21:57:14 +02:00
.gitignore feat(mandat): support arbitrary upload formats and refresh frontend tooling 2026-09-16 20:34:50 +02:00
Cargo.lock refactor(auth): avoid base32 round trip on 2FA setup, drop unused base32 dep, port test TOTP helper to totp-rs 6 2026-09-16 17:53:50 +02:00
Cargo.toml chore(deps): upgrade toolchain and dependencies, adapt auth to totp-rs 6 and rand 0.10 APIs 2026-09-16 17:52:26 +02:00
compose.dev.yml docs: overhaul local setup and harden Documenso development configuration 2026-09-16 21:57:14 +02:00
config.cfg feat: add assets_dir path to the config file 2025-08-03 15:45:21 +02:00
Dockerfile feat(mandat): support arbitrary upload formats and refresh frontend tooling 2026-09-16 20:34:50 +02:00
Dockerfile.rust.build chore: remove musl + add documenso public url env var added to private documenso 2026-03-06 17:15:35 +01:00
Dockerfile.web.build feat(mandat): support arbitrary upload formats and refresh frontend tooling 2026-09-16 20:34:50 +02:00
Makefile chore: remove musl + add documenso public url env var added to private documenso 2026-03-06 17:15:35 +01:00
README.md docs: overhaul local setup and harden Documenso development configuration 2026-09-16 21:57:14 +02:00
rust-toolchain.toml chore(deps): upgrade toolchain and dependencies, adapt auth to totp-rs 6 and rand 0.10 APIs 2026-09-16 17:52:26 +02:00
rustfmt.toml feat(api): convert to multi-crate workspace 2025-12-22 03:53:13 +01:00

Agir P.U.R.R

Agir P.U.R.R is a case-management application for creating, signing, and administering mandates. It consists of:

  • a Rust/Axum API with PostgreSQL storage;
  • separate Vue applications for users and administrators;
  • Documenso for electronic signatures;
  • AGE encryption for every uploaded and signed document.

The user and administrator APIs are separate runtimes of the same agird binary. Only the user API exposes the Documenso webhook.

Local architecture

Service Address Purpose
Documenso http://localhost:3000 Signature application
Agir PostgreSQL localhost:5433 Application database
Inbucket http://localhost:9000 Local email inbox
User API http://localhost:4040 User API and Documenso webhook
Admin API http://localhost:4041 Administration API
User frontend http://localhost:5173 User interface
Admin frontend http://localhost:5174 Administration interface

Documenso, its PostgreSQL database, and Inbucket run in Docker. The Rust APIs and Vue frontends run on the host for fast development.

Prerequisites

Install the following tools:

Tool Tested version Notes
Rust 1.98 Pinned by rust-toolchain.toml; nightly rustfmt is also required
Node.js 24 LTS Frontend runtime
pnpm 12.4.2 Pinned by app/package.json
Docker Current stable Docker Compose v2 is required
OpenSSL 3.x Creates local secrets and the signing certificate
AGE or rage Current stable Creates document-encryption identities
PostgreSQL client 18.x Optional, useful for diagnostics

Install the formatter used by this repository once:

rustup toolchain install nightly --component rustfmt

Quick start

1. Create the local configuration

cp .env.sample .env
cp app/.env.user.example app/.env.user
cp app/.env.admin.example app/.env.admin

Generate independent values for the placeholders in .env:

openssl rand -hex 32       # TOTP_ENCRYPTION_KEY
openssl rand -base64 32    # NEXTAUTH_SECRET
openssl rand -hex 32       # NEXT_PRIVATE_ENCRYPTION_KEY
openssl rand -hex 32       # NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
openssl rand -hex 32       # DOCUMENSO_WEBHOOK_SECRET

Copy each output to its matching variable. Do not reuse these example values in production and never commit .env.

For local HTTP development, keep COOKIE_SECURE=false. Production must use HTTPS and COOKIE_SECURE=true.

2. Generate the Documenso signing certificate

./scripts/gen-documenso-cert.sh

Enter a non-empty password and copy the same password to NEXT_PRIVATE_SIGNING_PASSPHRASE in .env. The script writes the ignored file data/certs/cert.p12. It must be a regular file, not a directory.

3. Start the Docker dependencies

docker compose -f compose.dev.yml up -d
docker compose -f compose.dev.yml ps

Check Documenso and its signing certificate:

curl --fail http://localhost:3000/api/health
curl --fail http://localhost:3000/api/certificate-status
docker compose -f compose.dev.yml logs --tail=100 documenso

The logs must not contain Certificate file not accessible.

4. Configure Documenso

Open http://localhost:3000, create the local Documenso account, and then:

  1. Create an API key and set it as DOCUMENSO_API_KEY in .env.

  2. Create a template containing at least one recipient with the Signer role.

  3. Add fields with these labels:

    Label Expected use
    data_controller Responsible organisation or person
    data_subject Mandate signer
    date_mandat Mandate date
    date_signature Signature date
  4. Set the template ID as DOCUMENSO_TEMPLATE_ID in .env.

  5. Create a webhook for the DOCUMENT_COMPLETED event:

    URL: http://host.docker.internal:4040/api/mandats/webhook/documenso
    Header: x-documenso-secret
    Secret: the DOCUMENSO_WEBHOOK_SECRET value from .env
    

localhost must not be used in the webhook URL: from the Documenso container, it refers to Documenso itself. Compose maps host.docker.internal to the host on supported Linux Docker engines and Docker Desktop.

Restart the Agir APIs after changing their .env configuration.

5. Initialize Agir

Apply the database migrations:

cargo run -- upgrade

Generate a local AGE identity:

age-keygen -o data/age-identity.txt
age-keygen -y data/age-identity.txt

The second command prints the public recipient. Register that age1... value:

cargo run -- keys add --public-key age1REPLACE_ME --label local-dev
cargo run -- keys list

The private identity decrypts local test exports. Keep it secret. AGE public keys are stored in PostgreSQL; there is no AGE_PUBLIC_KEY environment variable. The server refuses to start until at least one key is registered.

6. Run both APIs

Open two terminals from the repository root.

User API (binds to all host interfaces so the Docker webhook can reach it):

LISTEN_ADDR=0.0.0.0:4040 \
CORS_ORIGIN=http://localhost:5173 \
cargo run --features openapi -- server start --user

Admin API:

LISTEN_ADDR=127.0.0.1:4041 \
CORS_ORIGIN=http://localhost:5174 \
cargo run --features openapi -- server start --admin

The DOCUMENSO_API_URL and DOCUMENSO_PUBLIC_URL are identical warning is expected in this host-based local setup: both URLs are http://localhost:3000.

7. Run both frontends

Install dependencies once, then run each frontend in a separate terminal:

cd app
pnpm install
pnpm dev:user
cd app
pnpm dev:admin

The mode-specific environment files route each frontend to its matching API.

Verify the signing workflow

Use synthetic data and files only.

  1. Create an account at http://localhost:5173 and enable 2FA.
  2. Submit a mandate and complete the Documenso signing flow.
  3. Confirm Documenso marks the document as completed.
  4. Open the dossier at http://localhost:5174.
  5. Confirm the badge reads Signé via Documenso and shows a date.

The browser message Mandat envoyé means the dossier was submitted; it does not prove that Documenso sealed the PDF. The independent Documenso badge is updated only after Agir successfully processes DOCUMENT_COMPLETED, downloads the signed PDF, encrypts it, and stores it.

Verify the database if needed:

SELECT status,
       documenso_document_id,
       documenso_signed,
       documenso_signed_at,
       signed_document_file_name
FROM mandats_signatures
ORDER BY created_at DESC
LIMIT 1;

Expected results after completion:

  • status = 'signed' for a previously pending dossier;
  • documenso_signed = true;
  • documenso_signed_at is populated;
  • signed_document_file_name ends in .pdf.age.

For the full live verification workflow, use .private-data/docs/documenso-tracking-cnil-status-test-procedure.md.

Troubleshooting

cert.p12 is a directory

Docker creates a directory when a bind-mounted source path does not exist. Stop Documenso, remove only the empty incorrect directory, regenerate the certificate, and recreate the service:

docker compose -f compose.dev.yml stop documenso
rmdir data/certs/cert.p12
./scripts/gen-documenso-cert.sh
docker compose -f compose.dev.yml up -d --force-recreate documenso

Certificate file not accessible

Confirm the host path and container mount:

test -f data/certs/cert.p12
ls -l data/certs/cert.p12
docker compose -f compose.dev.yml exec documenso \
  ls -l /opt/documenso/cert.p12

The certificate must be readable and its password must match NEXT_PRIVATE_SIGNING_PASSPHRASE.

Signature remains pending in Agir

Check these states in order:

  1. Documenso completed document sealing without a certificate error.
  2. The Documenso webhook delivery succeeded.
  3. The webhook URL uses host.docker.internal:4040, not localhost and not the admin API.
  4. The webhook secret matches DOCUMENSO_WEBHOOK_SECRET.
  5. The user API log shows signed-envelope download and database updates.

After fixing delivery, redeliver the original DOCUMENT_COMPLETED event. A page refresh alone cannot update the stored signature state.

Local email is not delivered

Open http://localhost:9000. Documenso sends local mail to Inbucket over port 2500; it does not deliver messages to the public internet.

Development commands

Backend checks

cargo +nightly fmt --check
cargo clippy --workspace --all-targets --all-features
cargo test --workspace --all-features

Frontend checks

cd app
pnpm type-check
pnpm lint
pnpm format
pnpm build:user
pnpm build:admin

Generate TypeScript API clients

cargo run --features openapi -- export-openapi \
  --output app/openapi-user.json
cargo run --features openapi -- export-openapi \
  --output app/openapi-admin.json --admin
cd app
pnpm generate-client

Commit the OpenAPI documents and generated clients together with the API change that required regeneration.

Encryption-key management

# List configured public recipients
agird keys list

# Add a recipient
agird keys add --public-key age1... --label primary

# Remove a recipient by UUID or label; the last key cannot be removed
agird keys remove <id-or-label>

# Re-encrypt existing files for the currently configured recipients
agird keys regenerate --identity /secure/path/to/identities.txt

New uploads are encrypted for every active public recipient. Keep the private identities outside the repository and include them in the operational backup plan.

Production build

Production builds run in Docker and write artifacts to dist/:

# x86-64 Linux
make prod x86_64-unknown-linux-gnu

# ARM64 Linux
make prod aarch64-unknown-linux-gnu

Outputs:

  • dist/bin/agird;
  • dist/users.zip;
  • dist/admin.zip.

Docker BuildKit must support --output type=local.

Production deployment

  1. Back up PostgreSQL and UPLOADS_DIR.
  2. Install dist/bin/agird and unpack both frontend archives behind HTTPS.
  3. Provide secrets through a protected systemd EnvironmentFile= or an equivalent secret manager.
  4. Run dist/bin/agird upgrade before restarting the services.
  5. Register at least one AGE public recipient.
  6. Run distinct user and admin services with their own LISTEN_ADDR or UNIX_SOCKS_PATH and CORS_ORIGIN values.
  7. Route the public user /api path to the user service and the admin /api path to the admin service.
  8. Configure the public Documenso webhook against the user service only.
  9. Complete the private live-test procedure after deployment.

Example service commands:

dist/bin/agird server start --user
dist/bin/agird server start --admin

Do not use the local self-signed certificate, local database passwords, Inbucket, or development secrets in production.

Environment variables

Agir

Variable Required Description
DATABASE_URL Yes Agir PostgreSQL connection URL
LISTEN_ADDR Yes* TCP bind address
UNIX_SOCKS_PATH No Unix socket; takes precedence when set
UPLOADS_DIR Yes Encrypted document storage directory
CORS_ORIGIN Yes Exact frontend origin for this runtime
COOKIE_SECURE No Defaults to true; disable only for local HTTP
TOTP_ISSUER Yes Authenticator-app issuer
TOTP_ENCRYPTION_KEY Yes 64 hexadecimal characters
SESSION_TTL_SECONDS Yes Session lifetime
DOCUMENSO_API_URL Yes URL reachable by the Agir backend
DOCUMENSO_APP_URL Yes URL opened by the user's browser
DOCUMENSO_API_KEY Yes Documenso API credential
DOCUMENSO_TEMPLATE_ID Yes Mandate template ID
DOCUMENSO_WEBHOOK_SECRET Yes Expected x-documenso-secret value
CSP No Content-Security-Policy header value
RUST_LOG No Rust tracing filter

LISTEN_ADDR is still loaded when UNIX_SOCKS_PATH is used, so keep it set.

Local Documenso Compose stack

Variable Description
NEXTAUTH_SECRET Documenso session secret
NEXT_PRIVATE_ENCRYPTION_KEY Primary Documenso encryption key
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY Secondary encryption key
NEXT_PRIVATE_SIGNING_PASSPHRASE Password for data/certs/cert.p12
NEXT_PUBLIC_WEBAPP_URL Public Documenso URL; fixed locally by Compose

Consult the upstream Documenso documentation before adapting these local settings for production.