Skip to content

3.7 Programming Languages

Every language we add is one more set of tools, habits and dependencies to keep healthy. We therefore keep the list short and pick each language for a kind of work, not for a project.

3.7.1 Which Language for What

Kind of work Language Why
Internal critical systems: backends, workers, the farm sync client Rust Typed, memory-safe and fast; errors the compiler can find never reach production
Graphical interfaces, native and web Rust with Slint One language and one toolchain from the backend to the screen
Machine learning, internal tooling, scripts, prototypes Python The language of the machine learning ecosystem; quick to write
Simple web sites, and public web apps that people reach from search engines TypeScript with Nuxt The common tools of the web: pages that search engines index, accessible and quick to load

A prototype may be written in Python. When it becomes a critical system, it is rewritten in Rust rather than hardened in place.

3.7.2 Toolchains

  • The toolchain version is pinned in rust-toolchain.toml at the repository root.
  • cargo fmt --check and cargo clippy -- -D warnings run in CI; a warning fails the build.
  • Dependencies are reviewed before they are added, and Cargo.lock is committed for applications.
  • Projects and virtual environments are managed with uv; the Python version is pinned in .python-version.
  • ruff format --check and ruff check run in CI.
  • Public functions carry type hints.

3.7.3 Slint

Slint draws the interface itself, the same way on every platform, which is what lets one codebase serve desktop and web.

  • License. We use Slint under its royalty-free license for desktop, mobile and web applications. The license requires an attribution: the AboutSlint widget in an About screen (or the splash screen), or the Slint badge on a public page of the application.
  • Web. On the web, Slint runs as WebAssembly drawing on a canvas, which search engines cannot index and which must be checked for accessibility, text selection and load time. The Organization Dashboard is built with Slint (decided 2026-11-04): its users are signed in and work in it every day, so search indexing does not matter to it. A public app that people find through search is built with Nuxt.

3.7.4 Nuxt

Nuxt is for simple web sites, and for public web apps that people reach from search engines. The first site is the News site at news.moonitoring.com, a static site served from Cloudflare and kept in its own repository, moonitoring-app-news, apart from the backend of news and from the website; the first app is MooGrant, the web app of grant, which farmers open as guests from a search before they have an account.

  • When. The site publishes content and shares no code, rules or data with a Rust system; or the app is public, and search indexing, accessibility, text selection and the first load decide whether people use it, which are the points Slint on the web has still to prove (see Slint); or the app is where people sign in, register and accept invitations, the Account app, whose forms must work with the browser's autofill and password managers.
  • A Nuxt app is an interface like any other: it owns no data and decides no business rule, and it calls the systems' APIs with the same contracts and tokens as the other apps. Its repository is moonitoring-app-<name> (see Repository Naming).
  • When not. A critical system stays in Rust. An app used by signed-in people at work, such as the Organization Dashboard, stays in Rust with Slint.

3.7.5 Writing Rust

We use what makes Rust worth choosing: the compiler proves ownership, lifetimes and the shape of data, so we write code that lets it (decided 2026-11-10). The rules below come on top of cargo clippy, whose verdict still wins.

Features: build only what is used

  • Every dependency names the features it needs, starting from default-features = false, in [workspace.dependencies] of the repository's root Cargo.toml; a part adds features where it uses them. A default feature set is accepted only when the crate cannot be used without it, and the line says why.
  • Our own library crates put each optional part behind a feature (for example the gRPC server, the Dev ORM, a cloud client), so a consumer compiles only what it calls. Features only add: a feature never changes the behavior of code that does not use it, never removes an item, and builds alone and with every other feature (cargo hack check --each-feature where the crate has several).
  • No unused dependency: cargo machete runs with the checks, and cargo tree -e features is read when a dependency is added, so a feature enabled by one part does not silently grow another.
  • One version of each dependency in a workspace where it can be helped (cargo tree -d), and TLS through rustls only.

Ownership and lifetimes

  • Borrow by default. A function that only reads takes &str, &[T], &T or impl AsRef<…>; it takes ownership only when it keeps the value. A struct that only reads for a short scope holds a reference with a lifetime instead of an owned copy; Cow<'_, T> when a value is sometimes borrowed and sometimes built.
  • A .clone() is a decision, not a way to silence the borrow checker: cloning a cheap handle (an Arc, a pool, a client) is fine; cloning data to escape a lifetime is a design smell to fix.
  • Arc only for ownership that is really shared between tasks or threads, Arc<str> or Arc<[T]> for shared immutable data; no Rc<RefCell<…>> outside interface code that needs it, no 'static bound or Box::leak to make a lifetime go away.
  • Resources end with their scope (RAII): transactions, locks, temporary files, spans and connections are guards that release on Drop; a lock guard is never held across an .await, and a blocking call never runs on the async runtime (spawn_blocking).
  • Tasks have an owner: a spawned task is joined, cancelled through a token, or tracked by a JoinSet; shutdown waits for them with a deadline.

Types that say what they mean

  • Newtypes for identifiers, units and validated values (OrganizationId(Uuid), Bytes(u64)), built through TryFrom or a constructor that checks; enums instead of booleans and stringly typed states; #[must_use] on results and builders that must not be dropped; #[non_exhaustive] on public enums and structs of a library that will grow.
  • Typestate where an object passes through states with different operations (a builder that cannot build until its required fields are set, a release that cannot be approved before it is staged): the wrong call does not compile.
  • Iterators over index loops, const and const fn for what is known at compile time, impl Trait in argument and return position before boxing, generics before dyn unless the set of types is open.
  • Errors are typed (thiserror in libraries, an error enum per use case); no unwrap or expect on anything that comes from outside the process, and unsafe stays denied by the workspace lints.