Skip to content

Changelog

v1.3.0 (2026-08-23)

French and Italian, for the interface and for outbound email. Unlike the last two releases, this one changes what existing users see and receive — three behaviour changes take effect on upgrade without any config change on your side. They are listed under Changed below; read them before you pull. An English-only instance is unaffected: rendered English email is byte-identical to v1.2.0.

Added

  • French and Italian, bringing the bundled locales to four. Both cover the full interface and the built-in emails — one file per locale under app/i18n/, holding that language's UI strings plus an email subtree. Tone follows each language rather than being copied from German: French uses formal vous, German and Italian informal du / tu.
  • Built-in email translations. Previously the outbound templates existed only in English — there was no German set either, just per-locale NUXT_EMAIL_*_<LOCALE> env overrides. All five emails now ship translated in all four locales, sitting underneath the same override cascade as before: NUXT_EMAIL_*_<LOCALE>NUXT_EMAIL_* → built-in.
  • Shipping a catalogue does not enable it. NUXT_SUPPORTED_LOCALES still defaults to en; list a locale to let users store it and receive mail in it.
  • A :next container image on every push to main, plus an immutable :main-<short-sha> for the same build so a pre-release can be pinned or rolled back. :latest is untouched by these and still means "most recent release". See Deployment → Which tag to pull.

Changed

Three changes to behaviour, all of which apply on upgrade with no config change:

  • Users with a German, French or Italian locale now receive translated mail where they previously received English. If you had worked around this with NUXT_EMAIL_*_<LOCALE> overrides, they still win — nothing to undo.
  • Users with no stored locale now get NUXT_DEFAULT_LOCALE instead of English for the verification resend, the 2FA login code and the new-sign-in alert. Those four send paths bypassed the locale cascade entirely, which also meant a locale-suffixed env override never applied to them. users.locale is empty for accounts created before the column existed, so this can affect a meaningful share of an older instance.
  • Anonymous visitors now see the login page in NUXT_DEFAULT_LOCALE. The interface's last-resort fallback was hard-coded English, so a NUXT_DEFAULT_LOCALE=de instance greeted visitors in English while mailing them in German. If you relied on the English login page, set NUXT_DEFAULT_LOCALE=en.

The new-sign-in alert deliberately ignores the triggering request's Accept-Language, unlike every other send: that request is the suspicious one, and its headers must not choose the language of the warning the account owner has to read.

Fixed

  • The logo disappeared once you signed in. The account layout never rendered a brand block, so the mark vanished at the moment a user landed on /account.
  • The admin sidebar and mobile header rendered the logo and the app name side by side, which put the bundled mark next to an operator's own brand. Both now render one or the other, as the login and account pages already did.

Internal

  • The email copy moved out of a separate server-side module into the shared i18n catalogues, so a locale is one file rather than two. Note the two placeholder syntaxes this creates: UI strings use {single} braces, email atoms ones, matching the operator-facing env-override syntax.
  • Locale parity is now enforced in CI, not only by the type-checker: every bundled locale must carry exactly the English key set, no empty strings, and the same placeholder tokens per key.
  • Added the missing regression test for email escaping — the HTML body must escape firstName / ip / device while the text body keeps them raw.

v1.2.0 (2026-08-21)

Visual identity: the app finally looks like itself. Nothing to configure and nothing to migrate — an operator who has already set their own name, logo or theme keeps exactly what they had.

Changed

  • A stock install now shows a logo instead of the word "Schleuse". The mark — the two gate leaves of a canal lock, which is what Schleuse means — ships with the app and is used on the login pages, in the admin sidebar, and as the browser-tab icon, which previously did not exist at all: the favicon was only registered when a logo URL was configured.
  • The UI now ships its own typeface. Geist (and Geist Mono for code, identifiers and audit output), bundled with the app as two variable woff2 files, Latin subset, ~52 KB together. Previously no typeface was set at all, so the UI rendered in whatever the visitor's operating system supplied. Deliberately not loaded from a CDN: the login page makes no third-party request, which keeps it working air-gapped and stops visitors' IP addresses reaching a font host. Both faces are SIL OFL 1.1; the licence ships at /fonts/OFL.txt. The typeface is not configurable — say so if you need it to be.
  • NUXT_PUBLIC_THEME_LOGO_URL is unchanged and still wins when set. If you set NUXT_PUBLIC_THEME_NAME to your own brand without a logo, you keep seeing your name as text — the bundled mark is never shown above someone else's branding. Email is unaffected: the mail header still uses only an explicitly configured logo, and still falls back to the app name as text.

v1.1.1 (2026-08-21)

Internal only — nothing here changes how Schleuse behaves. No config, no API, no OIDC behaviour, no migration. Upgrading needs no action.

Fixed

  • pnpm db:migrate and its :make / :rollback / :down / :status siblings now run. They never had, since the first release — extensionless relative imports don't resolve under Node ESM, and the shared helpers.ts sat inside the migrations directory, where Knex loads every .ts as a migration. Development tooling only: the server has always applied migrations itself at startup, which is why this went unnoticed. Requires Node ≥ 22.18 (the published image runs 22.23).

Internal

  • Removed dead code: an unused error-logging helper, two unused validation helpers and two unused schemas, and a repository method with no callers.
  • Simplified the database write helpers — both carried a SQLite branch that ran the write and then a second SELECT to read the row back, which SQLite has not needed since it gained RETURNING. Verified to return byte-identical rows on SQLite and PostgreSQL, including booleans and timestamps.
  • Dropped the db:seed scripts, which pointed at a directory that never existed.

v1.1.0 (2026-08-21)

Added

  • SQLite Coolify templatedocker-compose.coolify-sqlite.yml, the single-container variant: no database service, one volume, NUXT_DATABASE_URL=sqlite:/app/data/schleuse.db. For a small instance with no separate database to run or back up. Note that /app/data then holds the database as well as the auto-generated keys, so losing it loses users, audit log, TOTP enrolments and passkeys at once — and tar/docker cp of a live WAL database is not a safe backup. The Postgres template keeps its filename; both cross-reference each other.

Changed

  • Email addresses are matched case-insensitively. Addresses are stored canonically (trimmed, lowercased) and every lookup canonicalises its input, so A@example.com and a@example.com are one account. Previously they were two, and a user who registered with one casing and typed the other got a generic "invalid email or password" — indistinguishable from a wrong password. Subaddressing (user+tag@…) and dots are preserved.
  • The OIDC email and preferred_username claims are now always lowercase. For users whose stored address was mixed-case the claim value changes once. sub is unaffected; a relying party keying on sub needs no migration.

Fixed

  • Corrected the public docs URL in the README (five links).

Upgrading

A migration canonicalises existing rows at first start. If two accounts hold addresses that differ only by case or whitespace they cannot both survive — the migration refuses to guess, aborts, and the container will not start until you reconcile them. Run the pre-flight query before upgrading; see Upgrading → duplicate email addresses.

v1.0.0 (2026-08-20)

First release.

A self-hosted identity provider: password and WebAuthn/passkey authentication, email verification, TOTP and emailed two-factor codes, and an OIDC provider (Authorization Code Flow with PKCE) for downstream applications.

Includes an admin panel (user management, audit log, integration guide, env configurator, SMTP and email diagnostics), a bearer-gated Prometheus endpoint, a provisioning API for migrating an existing realm without forcing a password reset, GDPR export and erasure, per-application OIDC roles, back-channel and RP-initiated logout, and a bilingual (en/de) interface.

See docs/implementation/v1.0.md for the design constraints and the decisions behind them.

A small but polished sidecar identity provider.