- Rust 47.8%
- Vue 25.7%
- TypeScript 24.8%
- CSS 0.7%
- JavaScript 0.5%
- Other 0.4%
| .config | ||
| .github/workflows | ||
| app | ||
| crates | ||
| migrations | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.sample | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| compose.dev.yml | ||
| config.cfg | ||
| Dockerfile | ||
| Dockerfile.rust.build | ||
| Dockerfile.web.build | ||
| Makefile | ||
| README.md | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
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:
-
Create an API key and set it as
DOCUMENSO_API_KEYin.env. -
Create a template containing at least one recipient with the Signer role.
-
Add fields with these labels:
Label Expected use data_controllerResponsible organisation or person data_subjectMandate signer date_mandatMandate date date_signatureSignature date -
Set the template ID as
DOCUMENSO_TEMPLATE_IDin.env. -
Create a webhook for the
DOCUMENT_COMPLETEDevent: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.
- Create an account at http://localhost:5173 and enable 2FA.
- Submit a mandate and complete the Documenso signing flow.
- Confirm Documenso marks the document as completed.
- Open the dossier at http://localhost:5174.
- 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_atis populated;signed_document_file_nameends 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:
- Documenso completed document sealing without a certificate error.
- The Documenso webhook delivery succeeded.
- The webhook URL uses
host.docker.internal:4040, notlocalhostand not the admin API. - The webhook secret matches
DOCUMENSO_WEBHOOK_SECRET. - 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
- Back up PostgreSQL and
UPLOADS_DIR. - Install
dist/bin/agirdand unpack both frontend archives behind HTTPS. - Provide secrets through a protected systemd
EnvironmentFile=or an equivalent secret manager. - Run
dist/bin/agird upgradebefore restarting the services. - Register at least one AGE public recipient.
- Run distinct user and admin services with their own
LISTEN_ADDRorUNIX_SOCKS_PATHandCORS_ORIGINvalues. - Route the public user
/apipath to the user service and the admin/apipath to the admin service. - Configure the public Documenso webhook against the user service only.
- 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.