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.yamlandpachifast.ymlare read. Aconfig.yaml,config.ymlor<directory>.yamlis ignored now. Rename it (mv config.yaml pachifast.yaml) or point to it withPACHIFAST_CONFIG_FILE=config.yaml. - Custom cache backend. The
Cacheinterface has a new abstract methodincr(key, ttl): an atomic counter that returns the new value and sets the expiry on the first increment. A backend of your own that subclassesCachemust implement it, or it cannot be instantiated. The memory and Redis backends already have it. /admin/audit/logsand/admin/usersare paginated. WithoutpageandpageSizethey answer page 1 of 25 rows, andpageSizeis capped at 200. A client that read the whole list must loop overpage(the response carriestotalandpages). Your own routes are unchanged unless you callbound_page_params().- Phone numbers.
phone_numberofPOST /mfa/sms/setupmust be international (+39 333 1234567); anything else is422. - Settings values.
ALGORITHMmust beHS256,HS384orHS512; the token lifetimes must be at least 1. - Removed.
SnakeCaseRouteandpachifast.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. RunPRAGMA foreign_key_check;on a copy first. swagger_noauthrefuses 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_secretgrows from 64 to 255 characters and is encrypted (Fernet, key derived fromMFA_ENCRYPTION_KEY, or fromSECRET_KEYwhen 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
hostis127.0.0.1(it was0.0.0.0) forpachifast devandpachifast serve. A service that must be reachable from other machines needs--host 0.0.0.0,PACHIFAST_HOST=0.0.0.0orhost: 0.0.0.0inpachifast.yaml. The container image and the generated pipelines already set it; a hand-writtenDockerfileor 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_enabledisfalse:POST /auth/registeranswers403 auth.registration_disabled. SetPACHIFAST_REGISTRATION_ENABLED=trueif you rely on self-service sign-up. - Removed settings
admin_emailanddata_path. If your code reads them, declare them in your own settings module; values in.envare still loaded. - SMS codes were removed in 0.7.1 and are back in 0.7.2 (
POST /mfa/sms/setupnow sends a code, and the method is active afterPOST /mfa/sms/confirm). Go straight to 0.7.2 or later. - Email MFA needs confirmation.
POST /mfa/email/setupsends a code; the method is inactive untilPOST /mfa/email/confirm.POST /mfa/{totp,email}/setupanswer403 mfa.disabledunlessMFA_ENABLED=true. Update the enrolment screens of your frontend. - SQL Server is gone: the
pachifast[mssql]extra,normalize_odbc18sql_urland themssql+aioodbcmapping. Supported databases are SQLite and PostgreSQL. - Concurrent refreshes with the same token (two tabs) no longer revoke the login: the second one gets
401but the session survives (refresh_reuse_grace_seconds, default 10). - Duplicate or dangling references in the admin routes answer
409 db.conflict/400 db.invalid_valueinstead of500.
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
- 2. Make the existing revisions independent of SQLModel
- 3. Port your models
- 4. Add the migration
- Back to the previous state
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 selectbecomesfrom sqlalchemy import select, andcol(x)is justx;- write column types explicitly when the schema must stay identical (
String(120),Float,UtcDateTime): SQLAlchemy 2.1 maps a PythonfloattoDouble, 2.0 toFloat.
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(componenttheme-toggle) is removed. The theme is nowappearance.theme(dark,lightorauto). A client that reads or writespreferences.thememust 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 (aLiteraloption, age/lelimit, the type) answers422 user.settings_value_invalidinstead of being saved.- New:
appearancesection,preferences.locale,timezone,dateFormat,tablePageSize, theapplyfield of the schema andPATCH /auth/user/settings.
A project that defined its own theme-toggle field keeps working: only the built-in preferences.theme is gone.