Thank you for your interest in contributing! Before you start, read through this guide to set up your environment and understand the project conventions.
Requirements: Nix with flakes enabled.
nix develop # enter the dev shell
process-compose up # postgres + the app, on http://localhost:8080Or nix run .#dev to do both at once. Migrations and seed data are applied for you, so there
is nothing else to run. pgweb is at http://localhost:8081 for browsing the database, and
process-compose up postgres migrate starts just a database when that is all you need.
Open http://localhost:8080/auth/login and click Sign in as Admin.
DATABASE_URLis exported automatically by the dev shell. Database state lives in.data/postgres;rm -rf .datastarts from scratch.
nix run .#postgresboots a PostgreSQL VM instead — slower, but it exercises the NixOS module used for self-hosting. See the README for that flow.
macOS
macOS requires nix-darwin for the
Linux builder, which is needed to run the e2e nixosTest (nix flake check)
and to cross-compile the server binary for Linux targets locally.
Enable the Linux builder in your nix-darwin configuration:
nix = {
# Required for the Linux builder to be trusted by the Nix daemon.
settings.trusted-users = [ "root" "@admin" ];
linux-builder = {
enable = true;
ephemeral = true;
maxJobs = 4;
config = {
virtualisation = {
darwin-builder = {
diskSize = 40 * 1024; # 40 GiB
memorySize = 8 * 1024; # 8 GiB
};
cores = 6;
};
};
};
};See the nix-darwin Linux builder docs for full configuration options and activation instructions.
Linux
Linux works out of the box with Nix and flakes enabled. Enable flakes in your Nix configuration if you haven't already:
nix.settings.experimental-features = [ "nix-command" "flakes" ];nix develop # enter the dev shell
process-compose up # postgres + hot-reloading dev server on :8080Tests:
cargo test -p horae-core # pure domain tests (no DB needed)
cargo test --features server # integration tests (needs Postgres with CREATEDB)
nix flake check # what CI gates on: tests, clippy, fmt, e2e nixosTestDo not use the include! macro (or include_str!) to embed documentation
from external files into doc comments or module-level docs. The macro resolves
at rustc compile time, which means Nix will re-hash and recompile the crate
whenever the included file changes — even though .md files are excluded from
the Nix source filter at the Nix evaluation level, the rustc invocation itself
sees the file as an input.
Write documentation directly in the source file instead.
Error types must be structs, not enums. Model them after
std::io::Error: a
single public struct with an opaque inner kind (a private enum or a boxed
trait object). This gives you:
- An opaque public surface — callers match on methods, not variants, so you can evolve the internals without breaking them.
- Room to add fields (e.g. context, spans, request IDs) without a breaking change.
- A natural
impl std::error::Errorimplementation.
// Preferred
pub struct AppError(AppErrorKind);
enum AppErrorKind {
NotFound(String),
Forbidden,
// ...
}
impl AppError {
pub fn not_found(msg: impl Into<String>) -> Self { ... }
pub fn is_not_found(&self) -> bool { ... }
}
// Avoid
pub enum AppError {
NotFound(String),
Forbidden,
}Branch naming: <username>/<issue> (e.g. alice/42)
If a PR is intended to close an issue, include a
closing keyword
in the PR description (e.g. Closes #42).