This directory contains the backend API server for Abacus, implemented in Rust. The backend is responsible for responding to API requests from the frontend, managing the database and completing requested user actions, including generating PDF and EML output files, computing summaries and apportionments, and much more.
During development the frontend assets are typically served by the frontend dev server. During production we serve the frontend assets directly from the Abacus binary. This results in production builds of Abacus that are completely self-contained.
You will need to install the following prerequisites before you can build and run the backend:
- Rust (stable) and Cargo
- SQLx CLI:
cargo install sqlx-cli
After having installed these prerequisites and having cloned the repository, you can quickly get started by running the following commands:
cd backend
sqlx database setup
cargo run -- --seed-dataThis will create the database, run the migrations, and seed the database with test data. The API server will now be available at http://localhost:8080. For more detailed instructions please read the rest of this README.
First run sqlx database setup to create the SQLite database. Then use
cargo build to build the project.
To make a release build, use cargo build --release.
The built binary will be located in target/release/.
Use cargo run to run the API on port 8080 (http://localhost:8080).
To let the API server serve the frontend, first compile the frontend using
pnpm build in the frontend directory. Then run the API server with the
memory-serve feature enabled:
cd frontend
pnpm install
pnpm build
cd ../backend
sqlx database setup
cargo run --features memory-servePDF generation uses a statically linked Typst, enabled by the embed-typst feature. That feature is part of the default
feature set. If you build with --no-default-features you have to enable it explicitly, otherwise generate_pdf is
unimplemented and PDF generation is unavailable. It can be combined with the memory-serve feature, e.g.:
cargo build --no-default-features --features memory-serve,embed-typstUse cargo clippy --all-targets --all-features -- -D warnings to lint the project. Warnings are treated as errors in the GitHub Actions workflow.
Use cargo test to run the tests. The tests are also run in a GitHub Actions workflow.
Debug builds are compiled with debug = "line-tables-only", which keeps panic backtraces with file and line
numbers but omits type and variable information. To inspect variables in a debugger:
-
Single command:
CARGO_PROFILE_DEV_DEBUG=2 cargo build(orcargo nextest run,cargo run). -
Entire workspace: create
backend/.cargo/config.toml(Git ignores the.cargodirectory):[profile.dev] debug = 2
In production, Abacus must be built with air gap detection enabled. To enforce air gap detection during build, enable the feature airgap-detection:
cargo build --release --features airgap-detectionTo enable air gap detection for a build without this feature, pass the CLI flag --airgap-detection. In development:
cargo run -- --airgap-detectionUsing a binary:
abacus --airgap-detectionIn production, Abacus must be built with TLS enabled, which makes Abacus serve
HTTPS only. To do this, enable the tls feature:
cargo build --release --features tlsOn startup Abacus loads (or, on first run, generates) a local certificate
authority under the directory given by --tls-dir (defaults to tls). A fresh
server (leaf) certificate is created in memory on every start, signed by the CA
and covering localhost, abacus.internal, and all routable LAN addresses.
To trust the server, import the CA into the client trust store: ca.pem on
Linux/macOS/Firefox, ca.cer (DER) on Windows. The CA can be downloaded from the
running server at /ca.pem and /ca.cer.
Verify that you have the right certificate by comparing its fingerprint. Abacus
logs both the SHA-256 and SHA-1 fingerprint of the CA on startup. Compare
against whatever your client shows: browsers and the OpenSSL CLI (openssl x509 -in ca.pem -noout -fingerprint -sha256) show SHA-256, while the Windows
certificate manager displays the SHA-1 digest as the certificate "thumbprint".
With the tls feature enabled, the default port is 8443 in debug builds and 443
in release builds. Binding to 443 requires elevated privileges (e.g. the
CAP_NET_BIND_SERVICE capability on Linux).
Alongside the HTTPS port, Abacus runs a plain HTTP server that provides the CA
certificate at /ca.pem and /ca.cer over plain HTTP (so clients can fetch it
before trusting the server) and redirects every other request to HTTPS. Its port
is set by --http-port / ABACUS_HTTP_PORT, defaulting to 80 in release builds
and 8080 in debug builds. The CA is served over HTTPS as well. Failing to bind the
HTTP port (for example without the required privileges) is logged but not fatal.
On Windows, AWS Libcrypto has some build requirements:
- C/C++ Compiler: these build tools have likely been installed during installation of Rust
- NASM, two options:
- Use the installer
- Or, use prebuilt NASM objects:
set AWS_LC_SYS_PREBUILT_NASM=1
You can use cross to compile for different architectures.
For example:
# build the frontend
cd frontend
pnpm install
pnpm build
cd ..
# build for ARMv6 32-bit Linux (like the Raspberry Pi 1/2/Zero)
cross build --release --features memory-serve,embed-typst,airgap-detection --manifest-path backend/Cargo.toml --target arm-unknown-linux-gnueabihf
# build for ARMv7-A 32-bit Linux (like the Raspberry Pi 3/4/5)
cross build --release --features memory-serve,embed-typst,airgap-detection --manifest-path backend/Cargo.toml --target armv7-unknown-linux-gnueabihf
# build for AArch64 64-bit Linux (Apple silicon)
cross build --release --features memory-serve,embed-typst,airgap-detection --manifest-path backend/Cargo.toml --target aarch64-unknown-linux-gnuTo use cross on Apple silicon, set the CROSS_CONTAINER_OPTS environment variable to --platform linux/amd64 when running the command.
The following dependencies (crates) are used:
argon2: password hashing implementation (Argon2id).async_zip: creating a zip of the EML_NL and PDF PV.axum-extra: handling for attachments and cookies inaxum.axum: web application framework that focuses on ergonomics and modularity.chrono: date and time library.clap: library for command-line argument parsing.cookie: dependency of axum_extra, for encoding and parsing cookies.hyper: fast and correct HTTP implementation.icu_collator: locale-aware string comparisonicu_locale_core: locale definitions foricu_collatormemory-serve: serves frontend assets from memory, but ad-hoc from disk during development.password_hash: password hashing interfaces.rand: create a random session key.serde_json: JSON support for Serde.serde: framework for serializing and deserializing data structures.sha2: generating a hash of the EML_NL XML files for inclusion in the PDF.socket2: Utilities for creating and using network sockets.sqlx: async SQL library featuring compile-time checked queries.strum: Converting enums from their string representation and backtokio-util: used for download streaming.tokio: runtime for writing asynchronous applications.tower-http: Tower middleware and utilities for HTTP clients and servers.tower: a library of modular and reusable components for building robust networking clients and servers.tracing-subscriber: utilities for implementing and composingtracingsubscribers.tracing: a framework for instrumenting Rust programs to collect structured, event-based diagnostic information.ttf-parser: for parsing TrueType fonts.typst-pdf: a PDF exporter for Typst.typst: a new markup-based typesetting system that is powerful and easy to learn.utoipa-swagger-ui: Swagger UI for the OpenAPI specification.utoipa: library for documenting REST APIs using OpenAPI.
The eml_signature crate (see its README) additionally uses:
aws-lc-rs: RSA PKCS#1 v1.5 signing and verification.cms: CMSSignedDatatypes for.signaturefiles.const-oid: OID constants for reading the certificate subject and key algorithm.der: ASN.1 DER primitives and PEM encoding.rcgen: RSA key generation and X.509 certificate building (aws-lc-rsbackend).x509-cert: parsing certificates.zeroize: wiping private key bytes on drop.
For TLS (HTTPS) support, when the tls feature is enabled, the following dependencies are used:
rcgen: X.509 certificate/DER generation (aws-lc-rsbackend)rustls-pki-types: shared certificate and private-key types, and PEM decoding.if-addrs: enumerating LAN IP addresses for the TLS certificate subject.rustls: TLS implementation, on the auditedaws-lc-rsprovider.axum-server: HTTPS serving foraxum, with graceful shutdown.
Additionally, the following development dependencies are used:
test-log: show tracing messages while running testsreqwest: HTTP client for testing the API.http-body-util: trait used to extract a response body in some tests.tempfile: to create temporary directories in tests.
SQLite is used as the database through the SQLx Rust crate.
An empty database is created as db.sqlite when the application is started.
The database schema is created and updated using migrations managed by SQLx.
When migrations are out of sync (e.g. VersionMismatch occurs when starting the API server),
the database can be reset using sqlx database reset or by running the API server with the
--reset-database flag.
Example database fixtures can be loaded during startup by adding the --seed-data command line
flag. This can be combined with the --reset-database flag to always start from a clean database,
e.g.:
cargo run -- --reset-database --seed-dataYou can use SQLx in offline mode so that you don't need an active database connection for compile-time query checks.
To compile in offline mode, set the SQLX_OFFLINE environment variable to true, e.g. env SQLX_OFFLINE=true cargo build.
The SQLx offline mode uses query metadata in the .sqlx directory.
Lefthook is configured to update the query metadata on commit, but you can also run cargo sqlx prepare -- --all-targets --all-features manually.
The utoipa crate is used to generate OpenAPI documentation for the REST API.
The OpenAPI JSON specification is available in the repository at openapi.json and can be found at /api-docs/openapi.json when running the API server.
The Swagger UI is available at /api-docs.
To update openapi.json in the repository, run the command cargo run --bin gen-openapi.
You can fill the database with test data and (optionally) export the election definition files with gen-test-election.rs.
Run cargo run --bin gen-test-election -- --help to see all command-line options.
The abacus binary supports a few arguments, which can be passed on the command line, or as environment variables:
Options:
-p, --port <PORT> Server port, optional [env: ABACUS_PORT=] [default: 8443]
-d, --database <DATABASE> Location of the database file, will be created if it doesn't exist [env: ABACUS_DATABASE=] [default: db.sqlite]
--tls-dir <TLS_DIR> Location of the TLS directory (CA certificate and key), will be created if it doesn't exist [env: ABACUS_TLS_DIR=] [default: tls]
--http-port <HTTP_PORT> Port for the plain HTTP server that serves the CA certificate and redirects to HTTPS [env: ABACUS_HTTP_PORT=] [default: 8080]
-a, --airgap-detection Enable airgap detection [env: ABACUS_AIRGAP_DETECTION=]
-V, --version Show version
-h, --help Print help
Note that airgap-detection is forced in our (pre-)releases. For release builds the default port number is 80, or 443 when TLS is enabled.
A development build also supports the following arguments:
-s, --seed-data Seed the database with initial data using the fixtures [env: ABACUS_SEED_DATA=]
-r, --reset-database Reset the database [env: ABACUS_RESET_DATABASE=]