Changelog
All notable changes to mp-server are documented here.
Format follows Keep a Changelog. Versions follow Semantic Versioning.
[0.23.1] — 2026-03-03
Added
- Scheduled worker that soft-deletes expired refresh tokens every 24 hours. Tokens that have naturally expired (e.g. user simply stopped using the app) but were never explicitly removed now get their
deleted_attimestamp set automatically, keeping session data consistent. Complements the existingcleanup_expired_tokenshard-delete job. - Comprehensive test coverage for the scheduler layer:
test_scheduler_entrypoint.py(27 tests) andtest_task_processor.py(9 tests) covering job registration, interval triggers, executor assignments, async/sync signature correctness, delegation, and exception handling for all scheduled jobs. - Scheduler documentation (
docs/scheduler.md) — architecture overview, registered jobs reference, how to add sync vs async jobs, executor routing, job defaults, error handling, and testing guide.
Changed
- APScheduler switched from
BackgroundSchedulertoAsyncIOSchedulerwith dual executors. Async jobs (outbox drain, webhook retry) run as coroutines directly on the uvicorn event loop viaAsyncIOExecutor. Sync jobs (heartbeat, enrollment processor, token cleanup, plugin registration) run in theThreadPoolExecutor. Previously all jobs ran on threads, and the two async jobs spun up a throwawayasyncio.run()event loop every 30 seconds — coroutines were silently never awaited when the scheduler was first migrated toAsyncIOSchedulerwithout the dedicated executor.
Fixed
- Async scheduler jobs (outbox drain, webhook retry) were silently not running. APScheduler 3.x does not auto-detect coroutine functions — without an explicit
AsyncIOExecutor, coroutines were dispatched to theThreadPoolExecutor, which called them as regular functions and discarded the unawaited coroutine objects. Added a named"asyncio"executor and routed both async jobs to it.
[0.23.0] — 2026-03-03
Added
- API keys (service tokens) for 3rd-party integrations:
- Long-lived bearer tokens with
mp_key_prefix for programmatic access. - Token acts as the creating user with a creator-defined subset of scopes.
- SHA-256 hash stored; plaintext shown once at creation.
- Creator-defined TTL via
expires_in(minimum 1 hour). - Effective scopes computed as intersection of key scopes and creator's current role scopes at validation time.
auth_path = "api_key"bypasses MFA and password-reset enforcement.- API endpoints:
POST /api/v1/api_key— create key (returns plaintext once).GET /api/v1/api_key— list current user's keys (paginated).GET /api/v1/api_key/{id}— retrieve a single key.DELETE /api/v1/api_key/{id}— revoke a key.
auth:api_keyscope required for all API key management endpoints.- Error catalog entries for API key errors (NOT_FOUND, REVOKED, EXPIRED, INVALID, SCOPE_EXCEEDS_CREATOR).
- Long-lived bearer tokens with
- Scope-based access control system (
@require_scopesdecorator) replacing coarse@require_roleschecks on all API endpoints. - Scope registry (
src/mp_server/auth/scope_registry.py) as the single source of truth for scope definitions and role-to-scope mappings. ScopeAccessEnforcerthat checks the current user's scopes against endpoint requirements at request time.current_scope_keys_varcontext variable populated during authentication.- DB-backed role-scope assignments via
role_scopetable for custom roles. - Scope API endpoints under the
Scopetag:GET /api/v1/scope— list all scopes from the registry.GET /api/v1/role_scope— merged role-to-scope mappings (registry + DB).GET /api/v1/role_scope_entry— paginated list of DB role-scope assignments.POST /api/v1/role_scope_entry— assign a scope to a role.GET /api/v1/role_scope_entry/{id}— retrieve a single assignment.DELETE /api/v1/role_scope_entry/{id}— remove an assignment.PUT /api/v1/role/{id}/scope— sync all scopes for a role (diff-based: adds missing, removes extra, keeps unchanged).GET /api/v1/role/{id}/scope— list scope keys assigned to a role.
RoleScopeRepository,role_scope_service, andScopeControllerfor CRUD operations.get_scopes_for_roles_with_db()merges Python registry and DB-persisted scopes during authentication.- Default role-scope seed data (
role_scope.yaml) — 122 entries for admin, provider, and integration roles. GET /api/v1/current_userresponse now includesscopesarray with the user's effective scope keys.- Scope documentation (
docs/scopes.md).
Changed
- All 16 controller files (~75 decorated methods) migrated from
@require_rolesto@require_scopes. AuthHandler.validate_bearerauthnow resolves scopes from roles via both registry and DB, and sets them on the request context.APIFacadewires the newScopeAccessEnforcerafter the existingRoleAccessEnforcer.
[0.22.12] — 2026-03-02
Added
- Seeder environment-specific overrides: seed data files now live under
data/default/and can be selectively overridden per environment (data/testing/,data/staging/,data/development/,data/local/). TheENVvariable controls which override folder is used. Environment files merge by recordid— matching ids replace the default, new ids are appended, and unmatched defaults are kept. Tables that only exist in an environment folder (no default file) are also seeded. - Environment seed data for
testing(provider, responder, integration users, active device with completed registration),staging,development, andlocal(team admin users with editor access on root folder). - Seeder documentation (
docs/seeder.md).
Changed
- Default seed data stripped to bare minimum: one admin user, one role binding, root folder with owner permission, and all enum/reference tables. Environment-specific users and data moved to their respective override folders.
[0.22.11] — 2026-03-02
Fixed
- Workflow simulation (
POST /api/v1/event_subscription/{id}/test) returned HTTP 500 when a node failed during the dry run. The error was caught and logged but then re-raised, propagating through the controller as an unhandled exception. Simulation mode now catches the error, records it in the node summary withstatus: "failed"and the error message, and returns the partial result so front-end developers can see which node failed and why. Errors likeTypeError: '>' not supported between instances of 'NoneType' and 'int'(from an unresolvedvarpath) now surface clearly in the response instead of crashing the endpoint. EmailServiceSMTP sequence was missing the requiredEHLOcommands. AddedEHLObeforeSTARTTLS(announce over plaintext) and after (re-announce over TLS), matching the SMTP spec. Some stricter servers would reject theSTARTTLScommand without a priorEHLO.
Changed
EmailServicerefactored: removed theencryptionconfig toggle — now always uses STARTTLS. Added module, class, and method docstrings. Switched from f-string logging to structured dict logging. HTML-escapedtokenandexpires_atin the password reset template to prevent XSS injection.
[0.22.10] — 2026-03-02
Added
- Questionnaire submission event enrichment:
medipal.submission.createdandmedipal.submission.anonymous_createdevents now includevariables_by_nameandfunctions_by_namelookups inevent.data.answers.scoring, re-indexing UUID-keyed variables and list-based functions by their human-readablename. This allows workflow JSONLogic expressions to reference scoring variables by name (e.g.{"var": "event.data.answers.scoring.variables_by_name.bmi.value"}) instead of requiring the internal UUID key. The original UUID-keyedvariablesand index-basedfunctionsare preserved unchanged. Events without ascoringblock are not affected.
Fixed
WorkflowRunNode.inputandWorkflowRunNode.outputwere alwaysnullin the database for most node types, making past runs impossible to debug.- Compute nodes now record
{"logic": <expression>}asinputso developers can see what JSONLogic was evaluated. - Delay nodes now record
{"delay_ms": <value>}asinput. - Action nodes already recorded input on success, but on plugin failure the resolved input was lost because the exception discarded the return value. Now pre-captured before dispatch so it survives failures.
- Failed nodes of any type now persist their pre-captured
inputalongside the error, so developers can see what data was being processed when the failure occurred. Previously_update_node_failedonly stored the error string.
- Compute nodes now record
- Workflow introspection controller methods (
api_v1_workflow_run_id_context_post,api_v1_workflow_evaluate_post) usedrequest.get()dict access on generated Pydantic request models, causingAttributeError: 'ApiV1WorkflowRunIdContextPostRequest' object has no attribute 'get'. Updated to use typed attribute access.
Changed
docs/workflows.mdupdated with new sections for debugging & introspection endpoints (sections 12–13), node input documentation by type, context snapshots in simulation responses, questionnaire submission scoring enrichment with usage examples, and a "Discovering Available Paths" guide.
[0.22.9] — 2026-03-02
Added
- Workflow debugging and introspection features for front-end developers building workflow node chains:
POST /api/v1/workflow_run/{id}/context— reconstruct the full workflow context at any node position in a past run. Returns the data structure and all available dot-notation variable paths for JSONLogic{"var": "..."}expressions. Requiresadminorintegrationrole.POST /api/v1/workflow/evaluate— sandbox for testing JSONLogic expressions without running a full workflow. Supports two modes: direct (provide data inline) or from-run (reference a past workflow run and node key to auto-reconstruct the context). Returns the evaluation result, the data used, and available variable paths. Requiresadminorintegrationrole.extract_var_paths()utility (mp_server.events.workflow.path_utils) — recursively walks nested dicts and returns all reachable dot-notation paths. Lists of dicts are merged by key (no numeric indices) so the front-end sees structural shape.reconstruct_node_context()andevaluate_expression()service methods onEventWorkflowService.
Improved
- Workflow simulation (
POST /api/v1/event_subscription/{id}/test) now includescontextandavailable_pathsin each per-node summary. The context snapshot is captured before each node executes, showing exactly what data was available at that point — including for skipped nodes. This is backwards-compatible (extra keys on the existingnodeslist items).
[0.22.8] — 2026-03-01
Improved
reconstruct_snapshotnow catchesTypeError,KeyError,AttributeError, andpydantic.ValidationErroralongside the existing JSON Patch exceptions, preventing 500s from corrupted or malformed revision data.reconstruct_snapshotguards metadata iteration: non-dictmetadata_changesand non-dict individual change entries are logged and skipped instead of crashing.- Revision patches with no
opskey are logged as warnings and skipped instead of silently applying a no-op. - Query validation error handler now uses defensive
getattrforallowed_valuesandfield, protecting against subclasses or mocks that skip__init__. - All
QueryValidationErrorraise sites in the query compiler now include thefieldkwarg:_apply_joins(join not defined),_compile_condition(in,not_in,betweenvalue-type checks), and the unhandled-operator fallback.
[0.22.7] — 2026-03-01
Improved
- Query validation error responses now include structured
detailswithfieldandallowed_values, so callers can see exactly which fields/operators are valid instead of guessing.
Added
docs/list-endpoints.md— front-end integration guide covering pagination, sorting, filtering, operators by column type, soft-delete controls, response shape, and validation errors.
[0.22.6] — 2026-03-01
Changed
- Standardised all
pylog.Loggerinstance names to use dot-separated paths mirroring the module location undermp_server/(e.g.Logger("questionnaire_service")→Logger("services.questionnaire"),Logger("SqlEngineFactory")→Logger("database.sql_engine_factory")). Redundant type suffixes (_controller,_service,_commands,_events,_store,_repo) are dropped when the parent package already conveys the type.
Fixed
- Workflow compute nodes now work correctly — the upstream
json-logicpackage (0.6.3) useddict.keys()[0], which is not valid in Python 3. Replaced with a patched local implementation atmp_server.events.workflow._json_logic.
[0.22.5] — 2026-03-01
Fixed
medipal.submission.createdandmedipal.submission.anonymous_createdevents now includeanswersin the event data payload, containing the raw questionnaire answers submitted by the user. Previously onlysubmission_idandquestionnaire_idwere emitted.
[0.22.4] — 2026-03-01
Added
- Questionnaire revision (change history) system: append-only log of RFC 6902 JSON Patch diffs recorded per mutation, enabling users to browse changes, inspect diffs, and restore to any previous state.
GET /api/v1/questionnaire/{id}/revision— paginated list of revisions with optional filters (operation,created_by, date ranges, sort).GET /api/v1/questionnaire/{id}/revision/{revision_id}— single revision detail.GET /api/v1/questionnaire/{id}/revision/{revision_id}/snapshot— reconstructed full questionnaire state at a specific revision point. Applies patches forward from the nearest anchor (parent snapshot or empty state).
- Revisions are recorded automatically for all questionnaire mutations:
EDIT,METADATA_EDIT,PUBLISH,FORK,DELETE,RESTORE,ARCHIVE. No-op edits (nothing changed) are silently skipped forEDITandMETADATA_EDIT; lifecycle operations (PUBLISH,FORK,DELETE,RESTORE,ARCHIVE) are always recorded. questionnaire_revisiondatabase table with JSONB columns forpayload_patch,config_patch,schedule_payload_patch, andmetadata_changes. Monotonically increasingsequenceper questionnaire andlineage_root_idfor cross-fork history.QuestionnaireRevisionRepositorywithget_next_sequence,get_revisions_up_to, andfilter_and_paginatemethods.questionnaire_revision_servicewithrecord_revision,record_fork,list_revisions_filtered,get_revision_or_error, andreconstruct_snapshotfunctions.QUESTIONNAIRE_REVISION_NOT_FOUNDerror code (404).jsonpatchdependency added.
[0.22.3] — 2026-02-28
Added
POST /api/v1/plugin_instance/{id}/invoke— execute an action on a configured plugin instance. Accepts anactionname and optionalpayload, validates against the action'sinput_schema, and returns the plugin response. Requiresadminorintegrationrole.- API endpoint schema-driven workflow documented in
CLAUDE.md— covers file naming conventions, request/response body schemas, and the generated submodule policy (mp_server_api,mp_server_pydantic_models,mp_server_sql_alchemy_modelsmust never be edited directly).
[0.22.1] — 2026-02-28
Added
- CloudEvents now published for all user lifecycle actions (
user.created,user.updated,user.deleted,user.password.changed) and folder lifecycle actions (folder.created,folder.updated,folder.deleted). Event definitions existed but were never wired into the service methods.
Fixed
test_encryption_servicemock used a flatconfig.get("vault.encryption_key")lookup, but the service does a two-stepconfig.get("vault").get("encryption_key"). Updated_mock_configto return a nested dict matching the actual lookup.
[0.22.0] — 2026-02-28
Removed
- Superset and nginx reverse proxy services (
proxy,superset,superset_init,superset_db) from all deployment compose files (development,staging,testing). Superset is now deployed separately. Thedemoandmainfiles already had no Superset containers. JWT_SECRETfallback inEncryptionService— the service now requiresvault.encryption_key(viaVAULT_ENCRYPTION_KEYenv var) with no fallback.- Superset infrastructure env vars (16 variables) from the deploy workflow. Only the 5 token broker vars remain (
SUPERSET_BASE_URL,SUPERSET_WEBSERVER_BASEURL,SUPERSET_USERNAME,SUPERSET_PASSWORD,SUPERSET_PROVIDER). - "Build and push Superset image to ECR" and "Copy nginx.conf to EC2" steps from the deploy workflow.
Added
VAULT_ENCRYPTION_KEY— dedicated env var for vault encryption, replacing the reuse ofJWT_SECRET.FRONTEND_BASE_URL— parameterised per environment (was hardcoded to the development URL).- Token TTL env vars (
USER_ACCESS_TTL,USER_REFRESH_TTL,DEVICE_ACCESS_TTL,DEVICE_REFRESH_TTL,PASSWORD_RESET_TTL) — previously hardcoded inauth_config.yaml. SUPERSET_GUEST_TTL_SECONDS— wired through as an env var (was hardcoded to300).- Superset token broker vars and
SEED_DEFAULT_PASSWORDadded todemoandmaincompose files (were missing). - SSH key cleanup step (
if: always()) in the deploy workflow. dotenvloading in testconftest.pyso new env vars are available without rebuilding the devcontainer.
Changed
docker-compose.main.yamlenvironment format normalised from- KEY=VALlist toKEY: VALmap style.docker-compose.main.yamlENVcorrected fromdevelopmenttoproduction.- App service in
development,staging, andtestingcompose files changed fromexpose: "8000"(behind proxy) to directportsbinding. - Deploy workflow:
appleboy/scp-actionpinned from@masterto@v0.1.7. - Deploy workflow: removed redundant
SIGIL_PAT,MEDIPAL_PAT,GITHUB_TOKENjob-level env vars (accessed directly via${{ secrets.* }}). - Deploy workflow:
FRONTEND_BASE_URLset per branch (likeSUPERSET_WEBSERVER_BASEURL).
[0.21.44] — 2026-02-28
Changed
EncryptionServicenow falls back to theJWT_SECRETenvironment variable whenvault.encryption_keyis not set in the config store, so existing deployments continue to work without updatingapp_config.yamlfirst.- Docker Compose stack simplified — removed the nginx reverse proxy and Apache Superset services (
proxy,superset,superset_init,superset_db). The application port (8000) is now exposed directly from theappservice. Addedadminerfor local database administration on port8080. .devcontainer/setup-hostnow quotes environment variable values in the generated.envfile, preventing shell-interpretation issues with special characters.
[0.21.36] — 2026-02-28
Added
refresh_expires_atandrefresh_expires_infields on all token-issuing endpoints:POST /api/v1/auth/login/credentials,POST /api/v1/auth/login/azure_ad,POST /api/v1/auth/login/mfa/verify_login,POST /api/v1/auth/device/login, andPOST /api/v1/auth/refresh. Previously only the access token TTL was returned — clients had no way to know when the refresh token expired.- Vault system: centralised, encrypted-at-rest store for secrets and variables.
POST /api/v1/vault_entry— create a vault entry. SECRET values are Fernet-encrypted (AES-128-CBC) before storage; VAR values are stored in plaintext.GET /api/v1/vault_entry— list vault entries withkind,key,enabled,search,sort_by, andsort_dirfilters, plus pagination.GET /api/v1/vault_entry/{id}— retrieve a single vault entry. SECRET values are masked as"******".PATCH /api/v1/vault_entry/{id}— partial update. Re-encrypts the value automatically when kind is SECRET. Re-checks key uniqueness when the key changes.DELETE /api/v1/vault_entry/{id}— soft-delete a vault entry.
$secret:<key>and$var:<key>reference syntax for vault entries. References are resolved at runtime in:- Plugin instance configs (
config_json) — replaces the TODO atplugin_service.py:233. - Workflow input mappings — runs as a second pass after JSONLogic evaluation.
- Plugin instance configs (
EncryptionService— Fernet-based encrypt/decrypt using theCONFIG_ENCRYPTION_KEYenvironment variable. Fails fast at construction if the key is missing.VaultEntryResolver— recursively scans dicts and lists for$secret:<key>/$var:<key>references, batch-loads entries, decrypts secrets, and replaces in a single pass. RaisesValueErrorfor missing or disabled entries.VaultEntrydatabase table and Alembic migration with partial unique index onkey(wheredeleted_at IS NULL).VaultEntryRepository(get_by_key) added toUnitOfWork.- Error codes:
VAULT.NOT_FOUND(404),VAULT.KEY_EXISTS(409),VAULT.DISABLED(422),VAULT.RESOLVE_FAILED(422). - All vault endpoints require
adminorintegrationrole. docs/vault.md— front-end integration guide covering all endpoints, reference syntax, plugin config and workflow examples, environment setup, and UI considerations.
Fixed
WorkflowExecutorfailed withField required [type=missing]forattemptswhen creatingWorkflowRunNoderecords. The entity schema declareddefault: 0but the code generator did not carry it into the Pydantic model. Added explicitattempts=0at the call site inexecutor.py.WorkflowExecutor._dispatch_nodecompared node types as lowercase ("action","end", etc.) but the database stores them as uppercase ("ACTION","END"). All nodes were silently skipped as "unknown". Fixed by normalizing with.lower()before comparison.SubscriptionRoutercalledexecutor.execute()synchronously inside the async event loop. Plugins that useasyncio.run()internally (e.g. the SMTP email plugin with aiosmtplib) crashed withRuntimeError: asyncio.run() cannot be called from a running event loop. Fixed by running the executor viarun_in_threadpool()so each plugin gets a clean thread with no active loop.QueryValidationError(e.g. unsupported sort field) was not caught by the global error handler, resulting in a 500 Internal Server Error. Added a dedicated handler that returns 400 with the validation message.
Changed
EncryptionServicenow reads its key fromvault.encryption_keyinapp_config.yamlvia the config store instead of theCONFIG_ENCRYPTION_KEYenvironment variable.EncryptionServicenow accepts any arbitrary string as encryption key (e.g.JWT_SECRET) and derives a valid Fernet key from it via SHA-256 + base64url encoding. Previously the configured value had to be a valid 44-character Fernet key, which meant reusing existing secrets likeJWT_SECRETwould fail at startup.- Workflow error logs now include full context:
subscription_name,event_type,event_subject,node_count,error_type, and full Python traceback. Previously onlysubscription_id,event_id, and a truncatedstr(exc)were logged, making failures difficult to diagnose.
[0.21.34] — 2026-02-28
Added
docs/workflows.md— comprehensive front-end integration guide covering all workflow, plugin, and event subscription endpoints. Includes request/response shapes, node type reference (ACTION, COMPUTE, SWITCH, JOIN, DELAY, END), execution engine internals, JSONLogic context structure, event processing pipeline, and UI considerations.
[0.21.33] — 2026-02-27
Added
GET /api/v1/folder_tree— permission-resolved folder hierarchy with nested questionnaires. Supportsinclude_questionnaires,questionnaire_status_id, andinclude_archivedquery parameters. Questionnaire nodes include all fields exceptpayload,schedule_payload, andconfig.sort_byandsort_dirquery parameters on all 21 list endpoints for server-side sorting.- Entity-specific filters across all list endpoints:
- Refresh tokens:
device_id,auth_path,expires_at_gte/lte - Enrollments:
enrollment_start_date_gte/lte,enrollment_end_date_gte/lte - Devices:
name,model,app_version,device_status_id,device_type_id,last_connection_date_gte/lte,search - Folders:
creator_user_id,search - Users:
mfa_enabled,force_password_change,ext_id,search,created_at_gte/lte - Roles:
name,search - User roles:
granted_by_user_id,granted_at_gte/lte - Questionnaires:
name,owner_id,search - Questionnaire submissions:
enrollment_id,created_at_gte/lte - Folder permissions:
permission_access_level_id,recursive - Questionnaire permissions:
permission_access_level_id - Plugin definitions:
plugin_key,name,search - Plugin instances:
search - Event definitions:
search - Event delivery tasks:
created_at_gte/lte,next_attempt_at_gte/lte,done_at_gte/lte - Event subscriptions:
name,search - Workflow runs:
event_type,started_at_gte/lte,completed_at_gte/lte - Webhooks:
name,url,search - Webhook deliveries:
status,event_type,http_status,http_status_gte/lte,delivered_at_gte/lte
- Refresh tokens:
Changed
GET /api/v1/workflow_runnow supportslimit/offsetpagination (default 25/0) withhas_next/has_previousin the response. Previously hardcoded to 250 results.GET /api/v1/webhook/{id}/deliverynow usesfilter_and_paginatewith full filter support instead of in-memory slicing.- Legacy adapter extracts
sort_by/sort_dirfrom filter dicts and converts them toSortSpecfor the query compiler. - Event definition listing supports in-memory text search and configurable sort direction.
[0.21.32] — 2026-02-27
Changed
GET /api/v1/event_delivery_tasknow supportslimitandoffsetquery parameters for pagination (default 25/0). Response includeslimit,offset,has_next, andhas_previousfields.
[0.21.30] — 2026-02-27
Changed
GET /api/v1/current_usernow returnsroles(array of full role objects) instead ofrole(single nullable object), correctly reflecting that a user can hold multiple roles simultaneously.get_roles_for_user(user_id)extracted intouser_role_serviceas a proper service function, removing directUnitOfWorkusage from the controller. The controller now delegates touser_role_service.get_roles_for_userin bothapi_v1_current_user_getandapi_v1_user_post.
[0.21.29] — 2026-02-27
Added
- Webhook system: Stripe-style signed HTTP delivery of CloudEvents to external endpoints.
POST /api/v1/webhook— register a new webhook. Returns the full HMAC-SHA256 signing secret exactly once.GET /api/v1/webhook— list webhooks with optionalenabledfilter and pagination.GET /api/v1/webhook/{id}— retrieve a single webhook (secret replaced with 4-character hint).PATCH /api/v1/webhook/{id}— update name, url, event_types, description, or enabled flag.DELETE /api/v1/webhook/{id}— soft-delete a webhook.POST /api/v1/webhook/{id}/rotate_secret— regenerate the signing secret; new secret returned once.GET /api/v1/webhook/{id}/delivery— paginated delivery history.GET /api/v1/webhook/{id}/delivery/{delivery_id}— single delivery record detail.POST /api/v1/webhook/{id}/delivery/{delivery_id}/retry— reset a failed delivery to PENDING for immediate retry.
WebhookDispatcherfans out CloudEvents to all matching registered webhooks on every outbox drain. Each delivery attempt is HMAC-SHA256 signed (X-Signature,X-Timestamp,X-Event-Type,X-Webhook-ID,X-Delivery-IDheaders).- Exponential backoff retry strategy:
next_attempt_at = now + 30 × 2ⁿseconds (30s, 60s, 120s, 240s, 480s); deliveries are marked FAILED after 5 attempts. - Scheduler job
webhook_retry_job(30-second interval) picks up PENDING deliveries viaSELECT FOR UPDATE SKIP LOCKEDand re-attempts them without blocking other scheduler instances. WebhookandWebhookDeliveryDB tables and Alembic migration.WebhookRepository(get_enabled_for_event_typewith JSONB containment check) andWebhookDeliveryRepository(lock_pending_batch) added toUnitOfWork.- All webhook endpoints require
adminorintegrationrole.
Changed
OutboxWorkernow also callsWebhookDispatcher.dispatch(event)for every drained CloudEvent, alongside the existingSubscriptionRouter.- CloudEvents are now fired for all questionnaire and submission lifecycle actions:
questionnaire.created,questionnaire.updated,questionnaire.published,questionnaire.deleted,questionnaire.restored,questionnaire.archived,submission.created,submission.anonymous_created,submission.updated,submission.deleted.
Fixed
WebhookService.list_deliveries:model_validatewas called on ORM objects after theUnitOfWorksession closed, causingDetachedInstanceError. Conversion to Pydantic models now happens inside the session.
[0.21.28] — 2026-02-26
Added
GET /docs/changelogendpoint servingCHANGELOG.mdas a rendered HTML page (via marked.js, no server-side dependencies).- Changelog link added to the OpenAPI description, visible in Swagger UI and ReDoc.
CHANGELOG.mdintroduced at the project root.
Changed
- All public functions in
questionnaire_submission_servicenow have complete docstrings (summary, Args, Returns, Raises) per project standards. sql_session_factory— added an explanatory comment forautoflush=False, clarifying that flush/commit lifecycle is managed explicitly byUnitOfWorkrather than triggered implicitly before queries.
[0.21.27] — 2026-02-26
Fixed
tenant_idinPOST /api/v1/auth/device/registerresponse was hardcoded to a literal string; it now reads fromapp_config.yamlviaget_config("app").tracker.tenant_id.questionnaire_submission_serviceraisedConflictErrorwith anAUTH_UNAUTHORIZEDerror code when a non-anonymous submission was attempted without a user ID. Changed toUnauthorizedErrorso the HTTP status (401) and error type are consistent.
[0.21.26] — 2026-02-25
Added
GET /api/v1/current_usernow includes arolefield in the response — the full role object (id, key, name, description, is_system, is_enabled) for the user's first assigned role, ornullif no role is assigned.- Testing-only
POST /testing/reset-dbendpoint: wipes the database, runs all Alembic migrations, and re-seeds with default data. Only registered and reachable whenENV=testing. Protected byX-API-Keyheader.
[0.21.25] — 2026-02-25
Added
- Admin observer endpoints for event and workflow monitoring:
GET /api/v1/event_delivery_task— list outbox tasks with optionalstatusandtargetfilters (admin only).GET /api/v1/workflow_run— list workflow runs, filterable byevent_subscription_idandstatus.GET /api/v1/workflow_run/{id}— retrieve a single workflow run by ID.GET /api/v1/workflow_run/{id}/node— list all node execution records for a run.
[0.21.24] — 2026-02-24
Changed
- Seeder improvements: default data and seed ordering updated.
[0.21.23] — 2026-02-24
Fixed
TypeError: Object of type datetime is not JSON serializableon all event-publishing paths. Root cause wasevent.model_dump()inoutbox.pywhen building the outbox message — changed tomodel_dump(mode="json")to produce ISO-string timestamps instead of rawdatetimeobjects.- Same fix applied to
WorkflowContext.to_logic_data()andWorkflowExecutorwhen storingtrigger_eventin theworkflow_runJSONB column.
[0.21.22] — 2026-02-24
Changed
- Workflow executor: plugin action node output is now round-tripped through
json.loads(json.dumps(..., default=str))before storing in theworkflow_run_node.outputJSONB column, preventing serialization failures from non-JSON-native plugin responses.
[0.21.21] — 2026-02-24
Added
- Workflow execution engine (
src/mp_server/events/workflow/):WorkflowContext— in-memory data structure carrying the triggering CloudEvent and accumulated node outputs, withto_logic_data()for JSONLogic evaluation.WorkflowExecutor— topological (Kahn's algorithm) graph traversal supporting action, compute, delay, switch, join, and end node types. Shared code path for real and simulation runs.SubscriptionRouter— matches incoming CloudEvents to activeEventSubscriptionrecords, evaluates source/subject/condition filters, and invokes the executor per match. Failures are best-effort (logged, not re-raised).node_validator— validates nodeconfigdicts against typed Pydantic models before saving to the database.input_mapping— resolvesinput_mappingfields via JSONLogic against the workflow context.
WorkflowRunandWorkflowRunNodeDB tables for tracking execution state and per-node results.WorkflowRunRepositoryandWorkflowRunNodeRepositoryadded toUnitOfWork.EventWorkflowServicefixes:- Added missing
patch_subscription_or_errorwrapper (previously called by the controller but not defined). replace_graphnow validates each node'sconfigbefore persisting.simulate()replaced with a real dry-run executor (no DB writes, no plugin calls, full graph traversal).
- Added missing
OutboxWorkerupdated to callSubscriptionRouter.route(event)for every drained event after local handlers complete.
[0.21.20] — 2026-02-24
Added
- CloudEvents emitted for all major domain actions: user created/updated/deleted, questionnaire published/archived, and questionnaire submission created/updated.
[0.21.19] — 2026-02-24
Changed
- Events system refactored: cleaner separation between event definitions, subscriptions, and delivery.
[0.21.18] — 2026-02-24
Changed
- Makefile restructured and CLI commands reorganised for clearer developer workflow.
[0.21.17] — 2026-02-24
Added
EventDeliveryTaskservice and repository for tracking outbox message delivery state.- Outbox worker (
OutboxWorker) with polling loop, drain-and-dispatch logic, and DONE/FAILED status updates. - Tests for event delivery task and outbox behaviour.
[0.21.16] — 2026-02-24
Changed
- Questionnaire controller: pagination, filtering, and response shape improvements.
[0.21.15] — 2026-02-24
Added
- Stamp audit fields (
updated_at,updated_by) now written on all mutation operations across the main domain entities.
[0.21.14] — 2026-02-24
Added
ip_addressanduser_agentcaptured at login and stored onRefreshTokenrecords for audit purposes.
[0.21.12] — 2026-02-24
Added
DELETE /api/v1/auth/device/{id}— remove a registered device.GET /api/v1/refresh_tokenandGET /api/v1/refresh_token/{id}— list and inspect issued refresh tokens.DELETE /api/v1/refresh_token/{id}— revoke a refresh token.- Refresh token revocation strategy: revoking a token also marks all child tokens (issued via refresh) as revoked.
[0.21.11] — 2026-02-24
Added
RefreshTokenAlembic migration, service, and repository. Refresh tokens are now persisted to the database and validated on the/auth/refreshendpoint.
[0.21.10] — 2026-02-24
Changed
- Submodule updates (
mp-server-api,mp-server-pydantic-models,mp-server-sql-alchemy-models).
Changelog tracking started at v0.21.10 (2026-02-24). Earlier versions are not documented here.