Overview
Start Here
Installation
Tutorial
Architect App Builder
Todo List App
Beacon Beats Audio Store
UCAL App
Authoring
Dynamic Apps
Static Apps
dev_docs
Why Pharos Feels Different
Architect Builder
Framework
Artifacts & Layout
Configuration
Outbound Email
How to Use CLI
Contracts
YAML Authoring
Action Hooks and Middleware
Feature Transaction Primitives
Generated Surfaces
Declarative CRM
Input Validation
Identity and Tenancy
UI Surface Library
Social Connections
Payment Provider Management
Integration Adapters
Reference
Contract, UI, and Surface Reference
Styling UI Elements
Architect Media Artifacts
API Docs
Use Cases
Dynamic SPAs
Generated Admin and APIs
Scheduling and Marketplaces
Multi-App Hosting
Release Notes
Vision
Pharos Framework

← Back

Action Hooks and Route Middleware

Pharos supports a narrow, typed extension layer around routes, actions, and queued workflow execution.

Use these seams to add request checks, bounded request shaping, and action-report metadata without turning the app into a custom middleware stack.

Pharos does not expose arbitrary app-owned imperative middleware injection.

Hook families

The supported hook families are:

  • route.request_hooks
  • route.middleware
  • action.hook_contract.derived_inputs
  • action.hook_contract.default_inputs
  • action.hook_contract.normalize_inputs
  • action.guard_contract.guards
  • action.hook_contract.report_annotations
  • action.hook_contract.failure_annotations
  • workflow queue metadata through queue_name, delay_seconds,
  • max_attempts, backoff_seconds, and lease_seconds

Use these when the behavior is:

  • local to one route, action, or workflow
  • declarative and bounded
  • explainable as contract rather than hidden runtime code

Example files

| Goal | Best first example | Why | |---|---|---| | JSON-only request admission | apps/dynamic_app/data/contracts/routes.yml | smallest explicit request_hooks example | | form-only request admission | apps/dynamic_app/data/contracts/routes.yml | smallest explicit form-body denial and allow example | | typed request and response middleware | apps/dynamic_app/data/contracts/routes.yml | shows both deny and allow paths | | derived request inputs | apps/dynamic_app/data/contracts/actions.yml | shows header and query values mapped into action input | | defaulted action input values | apps/dynamic_app/data/contracts/actions.yml | shows missing optional-submit values filled by declared defaults | | input normalization before mutation | apps/dynamic_app/data/contracts/actions.yml | shows trim and whitespace collapse without app-owned request glue | | aggregate pre-write guards | apps/commerce_spa/data/features/cart.yml | real cart cap example with negative path | | success and failure annotations | apps/dynamic_app/data/contracts/actions.yml | proves both success and rejection report metadata | | queue-aware workflow metadata | apps/ucal/data/contracts/workflows.yml | larger authenticated workflow proof |

route.request_hooks

Use route.request_hooks when a route should reject the request before normal action execution starts.

Supported request hooks:

  • json_body_required
  • form_body_required

Use it for:

  • generated or app-authored JSON mutation routes
  • routes that should not silently accept form or empty-body mutation requests
  • app-authored form mutation routes that should not silently accept JSON or
  • other body shapes

Working example:

  • apps/dynamic_app/data/contracts/routes.yml
  • apps/ucal/data/contracts/routes.yml

Validation path:

  1. Build the proving app.
  2. Run the local runtime gate.
  3. Send a non-JSON mutation request and confirm it is rejected.
  4. Send a valid JSON mutation request and confirm it succeeds.

Commands:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • wrong content type is rejected before mutation
  • valid JSON request succeeds through the shared runtime
  • form-only routes reject JSON requests with the documented failure
  • valid form request succeeds through the shared runtime

action.hook_contract.default_inputs

Use default_inputs when the action should supply a bounded fallback value only when the submitted field is missing or empty.

Supported sources:

  • literals
  • headers
  • query parameters
  • form fields
  • request method
  • request path

Behavior rules:

  • defaults are applied after derived_inputs
  • defaults are applied before required-field validation
  • defaults do not overwrite a submitted non-empty value

Working example:

  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • a missing snapshot_label is filled from the declared literal default
  • a submitted value is preserved and not replaced by the default

Use this seam when the fallback value is stable and declarative. Do not use it to hide request-derived business logic.

route.middleware

Use route.middleware when a route needs a bounded request or response rule that is still owned by the framework.

Supported request checks:

  • required header
  • required query parameter
  • required form field

Supported response shaping:

  • set static response header
  • mirror request header into a response header
  • mirror query parameter into a response header

Working example:

  • apps/dynamic_app/data/contracts/routes.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • missing X-Pharos-Proof on the proof route returns the documented denial
  • proof=open missing from the query route returns the documented denial
  • allowed requests return success plus the declared response headers

Use this seam when the rule is about admission or header shaping. Do not use it as a substitute for app-owned controller code.

action.hook_contract.derived_inputs

Use derived_inputs when the action needs a bounded value from the request context rather than direct form or JSON submission.

Supported sources:

  • literals
  • headers
  • query parameters
  • form fields
  • request method
  • request path

Target shape:

  • field names the action input field being populated
  • source keys name where the value comes from

Working example:

  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • the derived value reaches the action without app-owned request plumbing
  • missing or malformed required values fail through shared validation

Use this when the value is request-context metadata. Do not use it to smuggle in arbitrary runtime state.

action.hook_contract.normalize_inputs

Use normalize_inputs when an action should clean or canonicalize submitted string input before validation, mutation, and report annotation happen.

Supported normalization kinds:

  • trim
  • collapse_whitespace
  • lowercase
  • uppercase
  • empty_to_null

Working example:

  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • repeated whitespace in submitted task titles is collapsed before the record is
  • written

  • action reports observe the normalized value instead of the raw submitted value

Use this seam when the normalization rule is a bounded string cleanup step. Do not use it for arbitrary parsing, hidden lookup behavior, or app-owned request controller logic.

action.guard_contract.guards

Use guards when an action should fail before commit based on a bounded aggregate or existence check.

Supported guard kinds:

  • count
  • sum
  • exists

Supported bounds:

  • minimum
  • maximum
  • equals

sum guards may also use add so the pending write can be evaluated before the mutation is committed.

Working examples:

  • apps/commerce_spa/data/features/cart.yml
  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app commerce_spa --build-dir dist
sh scripts/check.sh commerce-spa postgresql-runtime

Expected result:

  • invalid mutation is blocked before commit
  • the rejection carries the declaration-local message
  • normal checkout behavior still stays green

action.hook_contract.report_annotations

Use success annotations when an action report should carry bounded metadata that helps operators understand what just happened.

Typical uses:

  • store normalized input
  • record transport classification
  • include request-derived fields that matter after success

Working example:

  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • action reports include fields such as normalized title and request transport

action.hook_contract.failure_annotations

Use failure annotations when a rejected or failed action should still leave a structured operator-facing report.

Typical uses:

  • attempted transition kind
  • approver or actor identity
  • resource identifier

Working example:

  • apps/dynamic_app/data/contracts/actions.yml

Validation path:

bin/pharos build app dynamic_app --build-dir dist
sh scripts/check.sh dynamic-app native-runtime

Expected result:

  • a rejected approval flow persists the declared failure metadata
  • failed mutation state is not committed just to preserve operator context

Workflow queue metadata

Use workflow queue metadata when queued or delayed work needs declared routing without exposing queue internals.

Supported fields:

  • queue_name
  • delay_seconds
  • max_attempts
  • backoff_seconds
  • lease_seconds

Working example:

  • apps/ucal/data/contracts/workflows.yml

Validation path:

bin/pharos build app ucal --build-dir dist
sh scripts/check.sh ucal workflow-jobs

Expected result:

  • queued work records lifecycle events
  • started, retried, failed, and succeeded paths remain observable through the
  • shared framework contract

  • emitted lifecycle payloads include the declared queue lane and retry-policy
  • metadata

When not to use these seams

Do not use these seams when you need:

  • arbitrary request-pipeline code
  • direct queue implementation control
  • raw socket or response-stream takeover
  • direct access to internal runtime structures

At that point the request is no longer “a narrow extension seam.” It is a framework feature request.

Related references

  • Extension seams
  • Generated surfaces
  • Feature transaction primitives
  • Integration adapters
  • Fixture and Surface List