Skip to content

Changelog

All notable changes to pachifast are documented here. The format follows Keep a Changelog and the project uses semantic versioning (while the major version is 0, minor releases may contain breaking changes).

[Unreleased]

Changed

  • New logo (an elephant in a Viking helmet): docs/assets/logo.svg and favicon.svg.

[0.15.0] - 2026-10-12

Added

  • pachifast.auth.profile.register_profile(): your own columns on the user, in a one-to-one table of the project. GET /auth/user shows a profile and PATCH /auth/user edits it (validated by your schema; 422 user.profile_not_configured when none is registered). The user table of the core stays fixed.
  • AppModule.requires_pachifast: a specifier of the pachifast versions a module works with, checked at startup with a message that names the module. Extension packages are named pachifast-<name> (see Extending the core).
  • db_pool_size, db_max_overflow, db_pool_timeout and db_pool_recycle: the connection pool of PostgreSQL, per process (the values used to be fixed at 5, 10, 30 and 1800).
  • pachifast audit purge [--days N] [--dry-run] and activity_log_retention_days: delete the activity log rows older than the retention.
  • An index on activity_log (user_id, timestamp). An existing project generates one migration for it (pachifast migrations revision --target-name auth --autogenerate -m "activity log index").
  • AppModule.setup_hooks: functions called with the app while it is built, to add a middleware or an exception handler (a startup hook is too late for that).
  • New dependency: packaging.

[0.14.0] - 2026-10-12

Added

  • pachifast.signals: lifecycle hooks a project connects to (USER_CREATED, USER_LOGGED_IN, LOGIN_FAILED, PASSWORD_CHANGED, USER_LOGGED_OUT). Handlers are functions or coroutine functions; an error in one is logged and does not touch the request. See Extending the core.
  • pachifast.auth.permission_provider.set_permission_provider(): a project says where the permissions of a user come from (route permissions=[...], the model admin and GET /auth/user use it). The default is still the database.

[0.13.0] - 2026-10-12

Added

  • GET /api/health/live (liveness: never touches the database) and GET /api/health/ready (readiness: 503 health.not_ready when the database does not answer within 3 seconds). GET /api/health/ is unchanged, and now logs why the database cannot be reached.
  • A request id: every response has an X-Request-ID header and the log lines of the request carry it (request_id_enabled, on by default; a sane id sent by the client is kept).
  • log_format: json, one JSON object per log line for a log collector.

[0.12.0] - 2026-10-12

Added

  • CI: Python 3.11 to 3.14, a lowest-direct job (the oldest allowed dependencies) and a coverage threshold of 85%.
  • Tests for the module loader, the discovery helpers, the health route, the permissioned router and the migration runtime (coverage 90% to 93%).
  • Documentation: a Stability and support page (what is stable, versioning, deprecation policy, supported Python, SQLAlchemy and databases), Upgrading with one section per version from 0.7, CONTRIBUTING.md, the install command for a git tag in both READMEs, a Without the CLI section in Getting started and the 90-day disclosure term in SECURITY.md.

Changed

  • The oldest allowed pydantic is now 2.11.0: with 2.10 a superuser was answered 403 on the permissioned routes.

Removed

  • pachifast.admin.cruds, an unused duplicate of pachifast.auth.cruds.get_users.

[0.11.0] - 2026-10-12

Added

  • pachifast.__version__ and the py.typed marker: the package already declared Typing :: Typed, but type checkers ignored its hints without the marker.
  • mfa_send_cooldown_seconds (default 30): a user waits that long between two codes sent by /mfa/email/setup and /mfa/sms/setup (429 auth.rate_limited). The two enrolment routes and their confirm routes also count against auth_rate_limit.
  • Cache.incr(key, ttl), an atomic counter with expiry (in the memory cache and in Redis). A custom cache backend must implement it.

Changed

  • Breaking: GET /admin/audit/logs and GET /admin/users answer page 1 of 25 when page and pageSize are missing, and at most 200 rows per page (they returned the whole table). paginate() itself is unchanged: the lists of your own routes stay unbounded unless you call bound_page_params().
  • Breaking: only pachifast.yaml / pachifast.yml are read as the configuration file. config.yaml, config.yml and <directory>.yaml are not any more: a file of another tool in the same directory (say a config.yaml with debug: true) was taken for settings. Use PACHIFAST_CONFIG_FILE for another name.
  • Breaking: phone_number of /mfa/sms/setup must be an international number (+ and 7 to 15 digits; spaces, dots, dashes and brackets are dropped). Anything else is 422.
  • ALGORITHM accepts HS256, HS384 or HS512; the token lifetimes must be at least 1.
  • Argon2 hashing (creating or updating a user, the backup codes, the email and SMS codes) runs in a thread, so a burst of registrations or enrolments does not stop the event loop.
  • SQLite connections enable foreign_keys (so ON DELETE CASCADE works), busy_timeout (5 s instead of "database is locked") and, for a file, journal_mode=WAL.
  • The rate limit of auth_rate_limit is one atomic counter per address and window (it used one cache entry per allowed request), and an IPv6 client counts as its /64 network.
  • POST /auth/logout closes the WebSocket connections of the user.
  • The activity log skips requests to paths that match no route (404, 405): a client that picks random paths no longer fills the table. A failed login is still logged.
  • swagger_noauth refuses a request that carries proxy headers (X-Forwarded-For, Forwarded, ...): behind a reverse proxy on the same machine the peer is always the loopback.

Fixed

  • The email and SMS code of POST /mfa/verify is claimed atomically: two parallel requests with the same correct code no longer both get a session.

Removed

  • SnakeCaseRoute and pachifast.utils.requests, unused and logging whole request bodies when switched on.

[0.10.1] - 2026-10-12

Fixed

  • A revision generated for the encrypted TOTP column says sa.String(length=255) instead of naming the pachifast class, so it does not depend on an import that the generated script did not have.

Changed

  • The documentation shows the version: in the footer and on the home page of the site, under the title of the reference pages and in README.md, README.it.md and docs/index.md. It is read from pyproject.toml (a MkDocs hook for the site, scripts/gen_reference.py for the markdown files, which a test keeps in step).

[0.10.0] - 2026-10-12

Added

  • PATCH /auth/user: a user edits their own firstName, lastName and email (never privileges or the username). Changing the email needs currentPassword, because the email receives the codes of the email MFA method.
  • The TOTP secrets are encrypted at rest (mfa_config.totp_secret, Fernet, key derived with HKDF from MFA_ENCRYPTION_KEY, or from SECRET_KEY when it is empty). New dependency: cryptography. The column is now 255 characters wide: an existing project needs one migration (pachifast migrations revision --target-name auth --autogenerate ..., see Upgrading from 0.9 in the authentication guide), then pachifast users encrypt-mfa-secrets encrypts the secrets saved as plain text. Plain values keep working until then. examples/demo has the migration.
  • SECURITY.md has a contact address and the list of known limitations.

Changed

  • Breaking: a password that breaks the policy is 422 on POST /auth/register and on the admin routes (it was 400), like on POST /auth/user/password. The code stays auth.password_too_short; it points at body.password on registration.

[0.9.0] - 2026-10-12

Changed

  • Breaking: the default host is 127.0.0.1 (it was 0.0.0.0), for pachifast dev and pachifast serve, and the generated pachifast.yaml says so. Reaching the server from another machine now takes --host 0.0.0.0, PACHIFAST_HOST=0.0.0.0 or host: 0.0.0.0; the container image and the generated pipelines already set PACHIFAST_HOST=0.0.0.0.
  • Changing the password to the current one is refused: 422 auth.password_unchanged.

Fixed

  • POST /auth/register takes the time of one password hash also when the username exists, so the response time no longer tells which usernames are taken.

[0.8.0] - 2026-10-12

Added

  • docs_enabled (default true): false removes the Swagger UI and every OpenAPI schema, so a public API does not publish its route list.
  • auth_rate_limit and auth_rate_window_seconds: a limit of requests per client address on POST /auth/token, POST /auth/register and POST /mfa/verify (429 auth.rate_limited, with Retry-After). Off by default; it counts in the shared cache, so use cache_url with several workers.

Fixed

  • GET /admin/audit/logs now applies the default activity_log_exclude_paths (refresh-token) like /activity, as the guide always said; excludePaths= (empty) shows everything.
  • The text search of the lists and of the model admin, and the path_contains filter of the activity log, treat % and _ as plain characters.
  • The model admin refuses at registration a column in exclude that is also in list_display, search_fields, list_filter, ordering, order_fields, readonly_fields or label_field; a label_field that is not a column is refused too.
  • A slow WebSocket client no longer holds up the event broadcast to the others (events go out in parallel, a client has 5 seconds to take one).
  • Fire-and-forget tasks (login event, activity log) are kept alive until they end.
  • Without an authentication database the 503 has a fixed message (db.not_configured); the reason goes to the log.
  • A startup warning when swagger_noauth or global_noauth is on without debug.

Tests

  • New suites for MFA, audit and authentication (about 120 tests): coverage of the package went from 80% to 89% (mfa 88-100%, audit 91-100%, auth 95-100%). pytest-cov is in the development group and the coverage configuration follows the greenlets and threads of SQLAlchemy, so pytest --cov=pachifast gives true numbers.

[0.7.2] - 2026-10-12

Added

  • SMS one-time codes are back (they were removed in 0.7.1): POST /mfa/sms/setup sends the code to the number and the method is active only after POST /mfa/sms/confirm receives it (400 mfa.sms_code_invalid, 400 mfa.no_sms_setup), like the email method. MfaSenderAdapter.send_sms_otp is the hook for the provider. The phone_number column never left the table, so accounts that were set up with SMS before 0.7.1 keep working.

Changed

  • The CI workflow runs lint and the tests on Python 3.11 and 3.13 with PostgreSQL, plus an optional run on SQLAlchemy 2.1.

[0.7.1] - 2026-10-12

Security

  • MFA is not enrolled while it is switched off. POST /mfa/{totp,email}/setup answer 403 mfa.disabled unless MFA_ENABLED=true: before, a user could enrol a second factor that the login never asked for.
  • Email OTP needs a confirmed address. POST /mfa/email/setup sends a code and leaves the method inactive until POST /mfa/email/confirm receives it (400 mfa.email_code_invalid, 400 mfa.no_email_setup). Before, a stolen access token could turn the second factor on with any address.

Fixed

  • Two refreshes with the same token at the same moment (two browser tabs) no longer revoke the whole login: a second use within refresh_reuse_grace_seconds (default 10, 0 restores the old behaviour) is refused with 401, the login survives.
  • A duplicate or a dangling reference in the admin of groups, permissions and users answers 409 db.conflict, and a value the database refuses answers 400 db.invalid_value, instead of 500. Names, descriptions and codenames now have a max_length in their schemas.
  • Alembic path_separator deprecation warning of the migration runner.
  • Broken anchor of the private index section in the publishing guide.

Changed

  • Python 3.11 or later is required (the scaffold already used tomllib, which does not exist on 3.10).
  • The generated pyproject.toml requires pachifast>=0.7.0.
  • The dependencies fastapi, fastapi-pagination, uvicorn and typer have an upper bound below 1.0; orjson (never imported) is gone and httpx moved to the development group.
  • pyproject.toml has [project.urls]; the source distribution holds only the sources, the README files, the license, the changelog and the demo.

Removed

  • Breaking: SMS one-time codes: POST /mfa/sms/setup, MfaSenderAdapter.send_sms_otp and the sms method. It may come back as a separate package; until then a project can add it itself. Accounts that were set up with SMS must be reset by an administrator (delete their mfa_config row); the phone_number column stays in the table so no migration is needed.
  • Breaking: SQL Server support: the pachifast[mssql] extra, pachifast.config.normalize_odbc18sql_url, the mssql+aioodbc driver mapping and the SQL Server label in the engine info. It may come back as a separate package; until then a project can wire the driver itself.
  • The stale MkDocs build site/ is no longer tracked.

[0.7.0] - 2026-10-08

Added

  • examples/demo: a complete, self-contained demo backend (core, MFA, model admin, an app module with its own tables and migrations, uploads, bulk actions, per-user settings). It is exercised by the test suite.

Changed

  • Breaking: registration_enabled now defaults to false: POST /auth/register answers 403 auth.registration_disabled unless PACHIFAST_REGISTRATION_ENABLED=true. A project that relies on self-service sign-up must set it.

Removed

  • Settings admin_email and data_path: the core never used them. A project that needs them declares them in its own settings (the .env values are still read, because the project settings accept extra variables).

[0.6.1] - 2026-10-07

Fixed

  • Model admin schema: with SQLAlchemy 2.1 the default of every field of a model was 5 (the internal marker DONT_SET) instead of the real default of the column.

[0.6.0] - 2026-10-07

Added

  • The user settings schema (GET /auth/user/settings/schema) gives every field that is not required its default, so a frontend can reset a form without copying the defaults.

Changed

  • Gitea workflow generated by pachifast new pipeline: the private index credentials fall back to REGISTRY_USERNAME/REGISTRY_TOKEN, and the optional variables BUILD_ADD_HOSTS and BUILDX_DRIVER handle registries with custom host names or a private certificate authority.

[0.5.0] - 2026-10-07

Added

  • Model admin: change history per row (GET .../{pk}/history, recorded with the change in one transaction; table pachifast_admin_change, migration target admin), bulk actions (@admin_action, built-in delete_selected, POST .../_actions/{name}), component: "lookup" for foreign keys to registered models, actions and history in the schema, pachifast locale admin, error code admin.action_not_found.
  • Model admin lists filter by range on numbers, dates and times (<column>__gte, <column>__lte), datetimes with an offset are stored as UTC in columns without time zone, error locations always use the schema's camelCase names and delete_selected answers a message.

[0.4.0] - 2026-10-07

Added

  • Generic model admin (optional AdminModule): @register_admin(Model) on a ModelAdmin gives list (search, filters, sorting, paging), detail, create, update and delete routes under /api/admin/models/{app}/{model}, a _schema for generic frontends, one permission per action (<app>.<model>.view|add|change|delete, created at startup) and writes in the activity log. See the Model admin guide.
  • GET /admin/models/{app}/{model}/_options (value/label pairs for selects) and type, default, primaryKeyType in the admin schema.
  • Error codes admin.model_not_found, admin.object_not_found and admin.integrity_error.

[0.3.0] - 2026-10-07

Added

  • New built-in settings section appearance (theme, accent, density, fontScale, reduceMotion) and new preferences fields locale, timezone, dateFormat and tablePageSize.
  • ui_field(..., apply="theme") names an applier the frontend runs with the value; exposed in the settings schema as apply.
  • A Literal setting is described in the schema as a select with its options (with optionKeys), ge/le as min/max of a number.
  • PATCH /auth/user/settings sets several values at once, all or nothing.

Changed

  • Breaking: preferences.theme (the theme-toggle component) is removed in favour of appearance.theme.
  • Setting values are validated against the field: a refused value answers 422 user.settings_value_invalid (loc: ["body", "value"]); PUT also accepts decimal numbers and null. A stored value that no longer validates reads as the default.

[0.2.2] - 2026-10-07

Fixed

  • POST /auth/user/password, POST /mfa/disable and POST /mfa/backup-codes/regenerate answered 400 while the account was locked: they now answer 429 with Retry-After (new code auth.too_many_attempts, mfa.too_many_attempts for the MFA routes) so a client can show a countdown. The login keeps its uniform 401.

[0.2.1] - 2026-10-07

Fixed

  • POST /mfa/verify answered inside the envelope while /auth/token does not: both login endpoints now answer the OAuth2 token shape (wrap_auth_token_response wraps both). A client that unwraps by shape is not affected.
  • The last accepted TOTP step is stored on the MFA row (last_totp_step), not in the cache: a code cannot be replayed through another worker or after a restart, and an older step is refused too. A migration is needed (one nullable column; autogenerate proposes add_column).
  • Argon2 checks of the MFA step (one-time codes, up to eight backup codes) run in a thread pool instead of blocking the event loop.
  • pachifast users checks the password against password_min_length (it had a fixed minimum of 8).
  • The core tables created at startup come from the core model modules, so a new core model cannot be forgotten (the refresh token table had been).

Added

  • Retry-After (seconds until the account is unlocked) on the 429 of a locked account at the second step of the login; the CORS middleware exposes it.
  • registration_enabled (default true): false answers 403 auth.registration_disabled on POST /auth/register.

Changed

  • init_db moved to pachifast.startup (the core module's startup hook already points there); pachifast.database no longer imports the feature packages, so the list of known layering violations is empty.

[0.2.0] - 2026-10-06

Breaking

  • SQLModel is gone. Models are SQLAlchemy 2.0 ORM classes on pachifast.database.Base (keyword-only dataclasses, so Python defaults apply at creation). AliasedSqlModel is removed; AliasedBaseModel stays and now reads ORM rows (from_attributes=True). The core tables keep the same columns and types (identical DDL on SQLite and PostgreSQL); the only schema change is the foreign keys listed under Fixed. Upgrading an application:
  • Rewrite the models with Mapped[...] and mapped_column() (see the Models section of the database guide). Integer auto-increment keys: mapped_column(primary_key=True, default=None, nullable=False).
  • Return Pydantic schemas (Schema.model_validate(row)) instead of table models; a model cannot be a response_model any more.
  • from sqlmodel import select becomes from sqlalchemy import select; col(x) is just x.
  • Existing migration files that contain import sqlmodel and sqlmodel.sql.sqltypes.AutoString(...): run pachifast migrations strip-sqlmodel (new). New migrations no longer import it.
  • Step by step: the Upgrading from 0.1.x guide in the documentation.
  • Database drivers are extras. asyncpg moved to pachifast[postgres]; aiosqlite stays a core dependency. PostgreSQL users must install pachifast[postgres].

Security

Fixes from a review against OWASP ASVS 4.0.3 level 1 (each one has a test in tests/test_security_hardening.py):

  • Lockout is atomic. Attempts are counted before the password is checked, in one UPDATE ... RETURNING, and refused in the same statement while the account is locked. Before, parallel requests could overwrite each other's counter and a burst could run far more password checks than login_max_attempts (a correct password was accepted after 20 wrong ones in a reproduction). A locked account now costs the same time as an unknown one. Password checks run in a thread pool, off the event loop.
  • MFA brute force. Wrong codes now count on the account as well as on the challenge, the attempt is spent before the code is checked, and logging in again no longer resets the counter. The password step gives its attempt back only because the second factor is pending.
  • TOTP codes and backup codes work once (the TOTP step is recorded in the cache; a backup code is consumed with a compare-and-set).
  • MFA cannot be replaced with an access token alone. POST /mfa/{totp,email,sms}/setup answer 409 mfa.already_enabled while MFA is active; POST /mfa/disable (password) is the only way. Disabling, regenerating backup codes and the new change-password route use the same counted password check.
  • The one-time code of an email or SMS login is stored before the response (it used to be created in a task that could lose the race with the client or outlive the database session).
  • swagger_noauth no longer trusts any Referer containing /docs: it needs a loopback client and the docs page as the path of the referer.
  • get_current_user_optional applies the revocation and active-user checks like get_current_user; get_current_ws_user honours events_allow_query_token.
  • orderBy accepts only mapped columns (orderBy=metadata was a 500) and the user list cannot sort by the password hash or the lockout counters (order_fields on the params class).
  • POST /mfa/totp/confirm answered 500 after enabling MFA (missing ok field).
  • Refresh tokens are recorded, single use and revocable. POST /auth/refresh-token returns a new refresh token and spends the presented one; presenting a spent token again revokes the whole login (reuse detection); POST /auth/logout revokes one login (body {"refresh_token"}) or all of them (no body); a password change or a deactivation revokes them all. New table refresh_token (a migration is needed) and setting refresh_token_rotation (default true). Tokens issued by 0.1.x are refused: log in again once. Before, logout revoked nothing and a refresh token could be reused for 7 days.

Changed (defaults)

  • password_min_length is 12 (was 8) and events_allow_query_token is false (was true): existing deployments that rely on the old values must set them explicitly.

Fixed

  • The foreign keys of group_permission, user_group and user_permission (group and permission columns) were never created because of a declaration mistake. They are now declared with ON DELETE CASCADE; existing databases need one migration (see Upgrading from 0.1.x).

Added

  • POST /auth/user/password (currentPassword, newPassword, 204): change your own password. Tokens issued before the change stop working.
  • pachifast migrations strip-sqlmodel to clean the revisions generated by 0.1.x; SQLite migrations are rendered in batch mode, so ALTER-style changes work there.
  • Synchronous sessions: get_sync_engine_from_url, get_sync_sessionmaker_from_url, sync_session_dependency_from_url, to_sync_url; extra pachifast[postgres-sync] (psycopg).
  • paginate_sync() for synchronous SQLAlchemy sessions, and pages in the list response.
  • Documentation site: mkdocs.yml (Material), the docs dependency group and a workflow that builds it and publishes it to GitHub Pages (.github/workflows/docs.yml), with a custom look: light and dark palettes, system fonts, a landing page.

Changed

  • List queries (paginate()) now delegate counting and paging to fastapi-pagination, which becomes a dependency. The query parameters and the {items, total, page, pageSize} response are unchanged; the list response gains pages.

Documentation

  • Frontend integration: when envelope_enabled=false, configure the client explicitly (QCLI_HTTP_ENVELOPE=false); shape-based unwrapping cannot tell a bare payload with success and data keys from an envelope.

[0.1.8] - 2026-10-06

Changed

  • The admin delete routes (user, permission, group) answer 204 without a body instead of 200 with an enveloped message, like POST /api/auth/logout.

Fixed

  • The Redis cache no longer needs Redis 6.2: the atomic read-and-delete is a MULTI/EXEC transaction instead of GETDEL (found by running against a real, older Redis server).

[0.1.7] - 2026-10-06

Added

  • Documentation in English: getting started, twelve guides and generated CLI, settings and message-code references; README.it.md; link checks in the test suite.
  • Message codes api.invalid_order_by (with params.field) and user.settings_key_invalid.

Changed

  • PUT /api/auth/user/settings/{category}/{key} accepts the key as camelCase (as in the schema) or snake_case; an unknown section or key is a 422 with a code instead of an unhandled error.
  • An invalid orderBy answers 422 with the code api.invalid_order_by and a localized message (it was a fixed Italian text without a code).

Fixed

  • get_current_active_ws_user, ActiveWsUserDep and PermissionedRouter.websocket(permissions=...) failed with a TypeError because the core session dependency required an HTTP request.
  • as_paginated_response raised UnboundLocalError when page or pageSize was missing.
  • pachifast users update --password now updates password_changed_at (revoking issued tokens) and lifts a lockout, like PATCH /api/admin/user/{id}.

[0.1.6] - 2026-10-05

Fixed

  • GET /api/version reports the installed version instead of a placeholder.

[0.1.5] - 2026-10-05

Added

  • One-time WebSocket tickets: POST /api/core/events/ticket, then /api/core/events/ws?ticket=.... Tickets are stored in the auth database (only their SHA-256) or in the cache (events_ticket_store); events_allow_query_token=false refuses the legacy ?token=.
  • pachifast.cache: a bounded in-memory cache or Redis (cache_url, cache_prefix, cache_max_entries, extra pachifast[redis]).
  • JSON Schema of pachifast.yaml (pachifast config schema), also written to new projects.
  • envelope_enabled setting: turn the {success, message, data} envelope off for the whole application.
  • ui_action(...): declarative actions in the user settings schema.
  • Documentation rewritten in English, with generated settings, CLI and message-code references.

Changed

  • The response envelope only wraps 2xx responses; a JSON error returned directly by an endpoint is no longer wrapped with success: true. Contract: every 2xx has success: true, every error is a non-2xx with {"detail": [...]}.
  • ui_field(events=..., watch=...) accept action names, never JavaScript code (ValueError otherwise).
  • Activity log: path, description and username are stripped of control characters and truncated.

Fixed

  • The auth migration target includes the activity log, MFA and WebSocket ticket tables, which Alembic projects never created.
  • The activity-log "skip" cache no longer grows without bound.
  • Pydantic V2 deprecation in pop_non_fields_from_dict_by_model.

[0.1.4] - 2026-10-02

Added

  • pachifast locale export --check lists the messages that are out of date.

[0.1.3] - 2026-10-02

Added

  • Translatable names of permissions and groups (pachifast locale permissions), exported with the catalogs.
  • The generated Dockerfile checks the message catalogs.

[0.1.2] - 2026-10-02

Added

  • --ca-cert and --index-name for pipelines that use a package registry served with a private certificate authority.

Changed

  • Application logs follow debug / log_level instead of always logging at DEBUG.

[0.1.1] - 2026-10-01

Added

  • Stable error codes, params and localized API messages (en, it); per-module catalogs, project overrides, pachifast locale check|export|settings.
  • POST /api/auth/logout, which emits a logout event.
  • WebSockets of deleted, deactivated or demoted users are closed (4401 / 4403).
  • pachifast users delete.
  • Gitea Actions pipeline generator and a private uv index for the image build.

Changed

  • debug is off by default.

Fixed

  • .env is found from the current directory at use time, not at import.
  • Events go to superusers and the user concerned only.

[0.1.0] - 2026-09-29

Added

  • Initial release: modular FastAPI framework with create_app, module registry, JWT authentication with RBAC, standard response envelope, multi-target Alembic migrations, audit log, events/WebSocket, MFA and the pachifast CLI (new, init, dev, serve, users, migrations).