Migration guide

This chapter is the cross-version migration reference. Each axess release that ships a breaking change documents the change here, with the symptom (what the compiler or the runtime will tell you), the rationale (why the change happened), and the fix (what to update in adopter code). The pattern is ordered by version, with the most recent breaks first.

The chapter is sorted by what you will see, not by what we changed. A breaking change manifests as either a compile error (the type system rejected something it accepted before), a runtime error (a deserialization fails, a config rejects), or a behaviour change (the same code does something subtly different). The sections below group by symptom; finding your case is faster than reading the full changelog.

Upcoming: 0.1.x to 0.2.0

The first crates.io publish is the 0.2.0 release. The accumulated changes since the previous stable line are catalogued exhaustively in CHANGELOG.md; this chapter covers the breaking ones an adopter has to act on.

Compile errors you will see

use axess::PolicyStore becomes use axess::AuthzStore. The authorisation entry point was renamed for consistency with the Authz* prefix convention. The new name better describes what the type is (an immutable store of policies plus schema, not just a policy collection).

use axess::AxessSession becomes use axess::AuthSession. The session extractor was renamed; the new prefix is the shared Auth* prefix from the naming conventions (Architecture at a glance).

use axess::backends::SqliteStore becomes use axess::backends::sqlite::SessionStore. The backend module layout was reorganised so the same trait name (SessionStore) appears under each backend's namespace; the previous flat SqliteStore symbol no longer exists.

AuthnService::new(backend) becomes AuthnService::new(identity_store, factor_store). The service now takes the two stores separately so adopters can wire different implementations (for instance, a read-replica identity store and a write-only factor store). When the two stores are the same type (the common case), pass it twice.

SessionLayer::with_secret becomes SessionLayer::with_signing_key. The previous name was ambiguous; the new name names what the bytes are used for (HMAC signing the cookie).

AuthState::Logged becomes AuthState::Authenticated. The state was renamed for clarity; nothing else changed about the variant.

Configuration changes

The axess_factors_default_password_hasher config function is gone. Argon2id is now the default; deployments that need a different hasher (PBKDF2, legacy bcrypt) implement a custom factor and register it. Factors and methods covers the extension pattern.

The AuditPipeConfig shape changed. The sinks: Vec<Box<dyn Sink>> field was replaced with explicit regulatory_sink: Arc<...> and analytics_sink: Option<Arc<...>> fields, reflecting the dual-stream architecture from Audit pipeline. The change makes the wire-stable vs. enriched stream distinction explicit in the config.

The RateLimitConfig no longer accepts a key_fn field directly; use KeyExtractor::Custom(Arc<dyn KeyExtractorFn>) to provide a custom extractor, or use one of the built-in variants (PeerIp, SessionId, UserId, TenantId, WorkloadId, Composite). The change is to make the common cases discoverable without losing the escape hatch.

Behaviour changes

The Authenticating state now carries a Vec<FactorKind> for remaining rather than the previous Option<FactorKind>. The change is what enables multi-factor methods longer than two factors. Code that pattern-matched on Some(kind) needs to adapt to remaining.first() or to iterate over the list.

The lockout policy now defaults to per-IP in addition to per-user and per-tenant. The previous default only locked the user; the new default also throttles the source IP. Deployments that explicitly want only per-user lockout configure LockoutPolicy::per_user_only().

The session cookie's SameSite attribute now defaults to Lax rather than Strict. The change is to match modern browser defaults and to admit cross-site link-to-app navigations as legitimate. Deployments that need Strict configure it explicitly.

The fingerprint binding now defaults to FingerprintPolicy::Warn rather than FingerprintPolicy::Reauth. The new default is quieter during initial rollout. Production deployments that want stricter posture lift to Reauth or Revoke after calibrating the warn rate (Cookies, fingerprinting, hijack detection covers the calibration).

Schema migrations

The users table gained a tenant_status field for the tenant-suspension support. The migration is a single ALTER TABLE that adds the column with a default value. The examples/sqlite/migrations/ shows the SQL.

The devices table gained a fingerprint_hash field and lost the previous fingerprint_raw field. The migration is destructive: the fingerprint_raw field carried PII that the new design hashes before storage (Device identity covers the rationale). Adopters who want to preserve the audit trail of past fingerprints write the migration accordingly; adopters who do not, just drop the column.

The authn_attempts table gained an event_kind enum field that distinguishes between attempt outcomes, rather than relying on a separate outcome string. The migration is non-destructive; the outcome field stays for backward compatibility and is populated from event_kind automatically.

The session-data schema version bumped from 1 to 2. The new version adds a device_id field on Authenticated (for the device-binding work covered in Device identity). The schema-migration code (Schema migration) handles existing sessions transparently; no manual data migration is needed.

Workspace structure changes

The axess-delegated crate folded back into axess-core. The adopter import paths stay the same (axess::delegated::* continues to work through facade re-export); the Cargo.toml no longer needs an explicit axess-delegated dependency, just the delegated feature on axess. The workspace dropped from 11 to 10 library crates.

For deployments running on the 0.1.x line:

The first step is to read this chapter end-to-end. Make a checklist of every change that applies to your code.

The second step is a parallel-deploy approach. Stand up a 0.2.0 build alongside the production 0.1.x; route a small fraction of traffic to it; observe behaviour. The session cookies between the two versions are not compatible (the schema-migration mechanism handles cookie reads but not writes across major versions), so the parallel deploy needs to be on isolated session storage.

The third step is the cutover. Once the 0.2.0 build has been green for at least the session TTL on the production-like sample, route 100% of traffic to it. The 0.1.x build can be decommissioned after a roll-back window has passed without incident.

The roll-back path: if 0.2.0 surfaces problems, route traffic back to 0.1.x; the sessions that started under 0.2.0 will be invalid against 0.1.x and will land as Guest, prompting re-login. The user-visible impact is one re-login; the behavioural impact is bounded.

Future migrations

The pattern from 0.1.x to 0.2.0 is the pattern future migrations will follow. Each migration documents itself here, sorted by release. The pattern:

Symptom: what the compiler or the runtime will tell you.

Rationale: why the change happened. Most changes happen because the previous shape was wrong in a specific way (a footgun, a performance bug, a security gap, an inconsistency with the rest of the library). The rationale gives the explanation; the next section gives the action.

Action: what to update in adopter code. The action is the shortest possible change that satisfies the new shape; longer restructurings are flagged as optional improvements.

A typical migration entry runs five to ten lines for a small change, a few paragraphs for a larger one. The chapter grows additively; older migrations are not removed.

What does not migrate

Some adopter changes do not produce a migration entry. The patterns:

Behaviour that was bug-fixed. A previous version's incorrect behaviour might have been load-bearing for an adopter who built around it; the fix is still the right thing to do, and the adopter has to adapt. The fix appears in the changelog as a bug fix; if the bug-fix is large enough to warrant a migration entry, it lands here, but not all of them do.

Internal refactors that do not change the public API. The internal split between axess-core modules is free to reorganise without producing a migration entry, as long as the public re-exports stay stable.

Configuration defaults that change but are configurable. A default that flipped is a behaviour change, captured above. A default that is configurable in both directions and the configuration is the source of truth does not produce a migration entry; the adopter's existing configuration continues to apply.

Further reading

Schema migration covers the per-session schema migration mechanism that handles session-data shape changes. The CHANGELOG.md covers the exhaustive list of changes per release; this chapter is the curated migration subset. Security posture covers the security-relevant breaking changes specifically, with the disclosure protocol for security fixes.