Skip to content

Upgrading

Read the section of every version you cross, from the one you run to the one you install, newest first. Each section says what changes and what to do. The complete list is in the changelog; the promises behind it are in Stability and support. While the major version is 0, a minor release may break.

After any upgrade of the core, check whether the database needs a revision for the auth target:

uv run pachifast migrations revision "check" --target-name auth --autogenerate   # an empty revision means nothing to do
uv run pachifast migrations upgrade --target-name auth

Delete the revision when it is empty. Only the versions that need an action are listed below; the others (0.10.1, 0.12 to 0.14, patch releases) are drop-in.

From 0.14 to 0.15

One action: the activity log has a new index on (user_id, timestamp), so the auth target needs one revision (run the two commands above and keep the revision). Nothing else is breaking: the new settings (db_pool_*, activity_log_retention_days), register_profile, AppModule.requires_pachifast and pachifast audit purge are opt-in.

From 0.10 to 0.11

Several defaults got stricter. Check these in order:

  • Configuration file name. Only pachifast.yaml and pachifast.yml are read. A config.yaml, config.yml or <directory>.yaml is ignored now. Rename it (mv config.yaml pachifast.yaml) or point to it with PACHIFAST_CONFIG_FILE=config.yaml.
  • Custom cache backend. The Cache interface has a new abstract method incr(key, ttl): an atomic counter that returns the new value and sets the expiry on the first increment. A backend of your own that subclasses Cache must implement it, or it cannot be instantiated. The memory and Redis backends already have it.
  • /admin/audit/logs and /admin/users are paginated. Without page and pageSize they answer page 1 of 25 rows, and pageSize is capped at 200. A client that read the whole list must loop over page (the response carries total and pages). Your own routes are unchanged unless you call bound_page_params().
  • Phone numbers. phone_number of POST /mfa/sms/setup must be international (+39 333 1234567); anything else is 422.
  • Settings values. ALGORITHM must be HS256, HS384 or HS512; the token lifetimes must be at least 1.
  • Removed. SnakeCaseRoute and pachifast.utils.requests: delete the imports.
  • SQLite. Connections now enable foreign_keys: an existing SQLite file that holds orphan rows can start to refuse writes that violate a constraint. Run PRAGMA foreign_key_check; on a copy first.
  • swagger_noauth refuses requests that carry proxy headers: behind a reverse proxy it no longer opens the docs without a login.

New: mfa_send_cooldown_seconds, /mfa/email/setup and /mfa/sms/setup count against auth_rate_limit. No schema change.

From 0.9 to 0.10

  • Encrypted TOTP secrets: one migration. mfa_config.totp_secret grows from 64 to 255 characters and is encrypted (Fernet, key derived from MFA_ENCRYPTION_KEY, or from SECRET_KEY when empty). New dependency: cryptography (installed with pachifast). Run:
uv run pachifast migrations revision --target-name auth --autogenerate totp_secret_encrypted
uv run pachifast migrations upgrade --target-name auth
uv run pachifast users encrypt-mfa-secrets

Plain-text secrets keep working until encrypt-mfa-secrets rewrites them. Do not change SECRET_KEY afterwards unless MFA_ENCRYPTION_KEY is set: with another key the secrets cannot be read and users must enrol again. Details in Authentication. - Weak passwords are 422. On POST /auth/register and the admin routes a password that breaks the policy answers 422 (it was 400); the code auth.password_too_short is the same. A client that tests for 400 must accept 422.

From 0.8 to 0.9

  • Default host is 127.0.0.1 (it was 0.0.0.0) for pachifast dev and pachifast serve. A service that must be reachable from other machines needs --host 0.0.0.0, PACHIFAST_HOST=0.0.0.0 or host: 0.0.0.0 in pachifast.yaml. The container image and the generated pipelines already set it; a hand-written Dockerfile or systemd unit does not.
  • Setting the password to the current one is 422 auth.password_unchanged.

From 0.7 to 0.8

Nothing breaks. New opt-in settings: docs_enabled=false removes the Swagger UI and the OpenAPI schemas (do it for a public API); auth_rate_limit and auth_rate_window_seconds limit login, registration and MFA verification per client address (use cache_url with several workers).

From 0.6 to 0.7

  • Python 3.11 or later. Upgrade the interpreter before the package.
  • Registration is closed by default. registration_enabled is false: POST /auth/register answers 403 auth.registration_disabled. Set PACHIFAST_REGISTRATION_ENABLED=true if you rely on self-service sign-up.
  • Removed settings admin_email and data_path. If your code reads them, declare them in your own settings module; values in .env are still loaded.
  • SMS codes were removed in 0.7.1 and are back in 0.7.2 (POST /mfa/sms/setup now sends a code, and the method is active after POST /mfa/sms/confirm). Go straight to 0.7.2 or later.
  • Email MFA needs confirmation. POST /mfa/email/setup sends a code; the method is inactive until POST /mfa/email/confirm. POST /mfa/{totp,email}/setup answer 403 mfa.disabled unless MFA_ENABLED=true. Update the enrolment screens of your frontend.
  • SQL Server is gone: the pachifast[mssql] extra, normalize_odbc18sql_url and the mssql+aioodbc mapping. Supported databases are SQLite and PostgreSQL.
  • Concurrent refreshes with the same token (two tabs) no longer revoke the login: the second one gets 401 but the session survives (refresh_reuse_grace_seconds, default 10).
  • Duplicate or dangling references in the admin routes answer 409 db.conflict / 400 db.invalid_value instead of 500.

From 0.1.x

The next release replaces SQLModel with the SQLAlchemy 2.0 ORM, adds the foreign keys that the association tables of the core were always meant to have and a table for the refresh tokens. The core tables keep the same columns, so upgrading is mostly a code change plus one migration.

1. Install the new version

Drivers are extras now. SQLite works out of the box; for PostgreSQL install pachifast[postgres] (asyncpg), and pachifast[postgres-sync] if you also use synchronous sessions. See Getting started.

2. Make the existing revisions independent of SQLModel

Revisions generated by 0.1.x start with import sqlmodel and use sqlmodel.sql.sqltypes.AutoString(...), which is a plain String. Without this step the first pachifast migrations upgrade stops with No module named 'sqlmodel'.

uv run pachifast migrations strip-sqlmodel --check   # lists the files that need it, exit 1 if any
uv run pachifast migrations strip-sqlmodel           # rewrites them (revisions and script.py.mako)

The result is the same schema: only the import and the type name change.

3. Port your models

Models are now plain SQLAlchemy classes on pachifast.database.Base; AliasedSqlModel is gone. See Models for the declaration style, and keep in mind:

  • an ORM model cannot be a response_model: declare a Pydantic schema (AliasedBaseModel, which reads ORM rows) and return that;
  • from sqlmodel import select becomes from sqlalchemy import select, and col(x) is just x;
  • write column types explicitly when the schema must stay identical (String(120), Float, UtcDateTime): SQLAlchemy 2.1 maps a Python float to Double, 2.0 to Float.

After porting, pachifast migrations revision "check" --target-name <target> --autogenerate must propose nothing for the tables you did not change. If it does, the declaration differs from the schema; fix the model, not the database.

4. Add the migration

One revision brings the new table and the foreign keys. Create it with autogenerate for the auth target; the paragraphs below explain what to check in it.

The refresh token table

Refresh tokens are recorded (single use, revocable at logout). Autogenerate proposes create_table('pachifast_refresh_token', ...) with its indexes; accept it as generated. Tokens issued by 0.1.x are not in the table and are refused, so every user logs in once after the upgrade.

The foreign keys

In 0.1.x the columns group_id and permission_id of the tables group_permission, user_group and user_permission were declared as foreign keys but, because of a mistake in the declaration, the constraints were never created. A deleted group or permission could leave rows behind. The models now declare them (ON DELETE CASCADE). Create the revision for the auth target:

uv run pachifast migrations revision "refresh tokens and association foreign keys" --target-name auth --autogenerate

Then edit the generated file: add the clean-up of the orphan rows at the start of upgrade(), otherwise PostgreSQL refuses to create a constraint while rows violate it. Those rows grant nothing, so deleting them is safe.

ORPHANS = (
    ("pachifast_group_permission", "group_id", "pachifast_group"),
    ("pachifast_group_permission", "permission_id", "pachifast_permission"),
    ("pachifast_user_group", "group_id", "pachifast_group"),
    ("pachifast_user_permission", "permission_id", "pachifast_permission"),
)


def upgrade():
    for table, column, parent in ORPHANS:
        op.execute(f"DELETE FROM {table} WHERE {column} NOT IN (SELECT id FROM {parent})")
    # ... the generated op.batch_alter_table(...) calls follow

Adjust the table names if you changed table_prefix. The generated downgrade() drops the constraints by name (the models name them fk_<table>_<column>), so it works as generated.

On SQLite the revision is rendered with batch_alter_table (the table is rebuilt), which is why the migration environment enables batch mode for SQLite by itself.

Check the result before applying it to a real database: take a copy, add a row that points to a missing group, run upgrade, and confirm the row is gone, the constraints exist and a second upgrade after a downgrade works.

Back to the previous state

pachifast migrations downgrade <previous revision> --target-name auth removes the constraints; nothing else in the schema changed. Going back to 0.1.x also needs the old sqlmodel dependency and your old models.

From 0.2.0 to 0.2.1

One more column: last_totp_step on the MFA table (nullable, so existing rows are valid). Create the revision with autogenerate and apply it:

uv run pachifast migrations revision "totp last step" --target-name auth --autogenerate
uv run pachifast migrations upgrade --target-name auth

POST /mfa/verify now answers without the envelope, like /auth/token; a client that unwraps by shape needs no change.

From 0.2.x to 0.3.0

No database migration. The change is in the user settings:

  • preferences.theme (component theme-toggle) is removed. The theme is now appearance.theme (dark, light or auto). A client that reads or writes preferences.theme must switch; values already stored under it stay in the JSON and are ignored.
  • PUT /auth/user/settings/{category}/{key} validates the value: what a field does not accept (a Literal option, a ge/le limit, the type) answers 422 user.settings_value_invalid instead of being saved.
  • New: appearance section, preferences.locale, timezone, dateFormat, tablePageSize, the apply field of the schema and PATCH /auth/user/settings.

A project that defined its own theme-toggle field keeps working: only the built-in preferences.theme is gone.