Descriptor-driven admin
Lists, forms, filters, and navigation are serde descriptor structs.
Generic handlers render them, so a new screen is a value you write, not a
controller you wire by hand. Descriptors are YAML-loadable, so screens can
live in configuration. Field types are an open registry (text inputs,
select, a searchable reference picker), list cells render by column type,
and a form's writes go through a persister you can swap.
Portable data layer
Queries and migrations are built with sea-query over
sqlx and run unchanged on PostgreSQL, MySQL, and SQLite.
Backend quirks (booleans, key columns, casts, upserts, returning ids)
are handled by polyfills, so you write natural Rust types. The full test
suite runs against all three databases on every push.
Reversible migrations
Each migration is a Rust unit with up and down.
A runner applies them in order and tracks history per module, so a module
carries its own version history and can be rolled back, reset, or
refreshed from the command line.
Role-based permissions and an audit log
Permissions are dotted strings carried by descriptors and enforced by
middleware. Roles group them; per-user overrides take precedence when you
need to grant or revoke for one account. A screen with a form must declare
its permission, or the app refuses to boot. Every privilege and data
mutation lands in an append-only audit log, with a read-only view in the
admin for anyone permitted to see it.
Localized from the start
Every user-facing string in the admin is a translatable value: a bare
string does not compile. Each request resolves a locale from the
operator's preference, the browser's language, and the application's
default, and dates render in that locale. Translations are standard PO
catalogs contributed by modules, so a new language is a file, and
lat i18n extracts, checks, and reports coverage.
Plugins as folders
A plugin is a crate under plugins/<author>/<plugin>/.
lat plugin sync registers it, the admin enables or disables
it, and its migrations and screens arrive with it. Plugins are isolated at
boot: one that fails is quarantined with its dependents and logged, and
the rest of the application comes up. Every plugin declares the database
capabilities it needs, checked before it loads.
Forms that validate, sessions that protect
Form submissions run through a rule engine (required, length, email, URL,
unique) and re-render with per-field errors. Every admin form carries a
CSRF token and every request passes an origin check, with flash messages
that survive the redirect after a save. Credentials are hashed with
Argon2id, and repeated failed logins lock an account without needing Redis.
Server-rendered, no JS framework
The admin is Askama templates with HTMX. Interactions happen over small
HTML swaps, so there is no separate front-end build, no client state to
keep in sync, and one small vanilla-JS island only where a widget needs
it.
A command-line tool that finds its way
lat new is a guided installer: it prompts for the application
name, timezone, and database, creates the database, applies the
migrations, and creates the first administrator. lat serve
runs the app, lat doctor checks that it is ready, and
lat admin creates or recovers an administrator. Run from
anywhere inside an application, each command reads what the app already
states, its config and its database, and asks only for what it cannot
find.
On the road to 1.0
Laterite is pre-1.0 and ships in themed minor releases. Next come CMS
pages and themes with a runtime template engine; then mail, background
jobs, realtime, and cache as swappable drivers, with a database-backed
default so a PostgreSQL deployment needs nothing else and Redis stays
optional; then user-defined content types that become real tables; and
the plugin marketplace that 1.0 freezes the public API for. The
changelog records each step as it lands.