A backend service that orchestrates adaptive, forms-based user journeys using event sourcing and Command Query Responsibility Segregation (CQRS). Journey routes are determined dynamically by a GoRules decision engine, and all personally identifiable information (PII) is protected by General Data Protection Regulation (GDPR) crypto-shredding.
- Rust (stable)
- Docker (for PostgreSQL)
sqlx-cli
cargo install sqlx-cli# Start Postgres
docker-compose up -d
# Create the database and run migrations
cargo sqlx database create
cargo sqlx migrate runexport DATABASE_URL=postgres://postgres:postgres@localhost:5432/journey_dynamics
# 256-bit Key Encryption Key for GDPR crypto-shredding (required)
export JOURNEY_KEK=$(openssl rand -base64 32)
# Path to the AttributeSchema JSON that classifies each attribute path as
# plaintext or per-subject secret (optional). When unset, the service runs
# with a permissive schema that treats every path as plaintext.
export JOURNEY_ATTRIBUTE_SCHEMA_PATH=./attribute_schema.json
JOURNEY_ATTRIBUTE_SCHEMA_PATHcontrols howSetAttributesroutes each path. The permissive default is convenient for local development, but in production you should supply a schema so that PII paths are encrypted under the right subject's DEK. See the migration guide for the file format (permissive,plaintext_prefixes,namespace_patterns, exactpaths).
Keep
JOURNEY_KEKsafe. It wraps every per-subject Data Encryption Key stored in the database. Losing it makes all encrypted PII permanently irrecoverable. In production, load it from a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) rather than an environment variable.
cargo run -p journey_dynamics
# Listening on 0.0.0.0:3030curl -i -X POST http://localhost:3030/journeysReturns 201 Created with a Location: /journeys/{journey_id} header.
curl http://localhost:3030/journeys/{journey_id}The response includes shared_data (the merged path-keyed attribute bag),
persons, and latest_workflow_decision. The decision now carries a
phase label alongside suggested_actions — read phase to drive UI
state. The top-level current_step field is deprecated (it is still
populated for legacy StepProgressed events); prefer
latest_workflow_decision.phase.
SetAttributes accepts a flat map of path → value (or the nested sugar form below) and
routes each attribute to plaintext storage or per-subject encrypted storage based on your
AttributeSchema.
A single submission can touch attributes for multiple data subjects atomically.
The nested sugar form (server-side flattened) is the most ergonomic on the wire:
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"SetAttributes": {
"search": {
"tripType": "round-trip",
"origin": "LHR",
"destination": "JFK",
"departureDate": "2025-08-15",
"passengers": {
"total": 1,
"adults": 1,
"children": 0,
"infants": 0
}
}
}
}'The canonical flat form is also accepted:
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"SetAttributes": {
"changes": {
"search/origin": "LHR",
"search/destination": "JFK"
}
}
}'For per-person PII (passport number, date of birth, …) call CapturePerson first to
bind a subject_id to the person slot, then use paths under persons/<ref>/…:
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"SetAttributes": {
"persons": {
"lead_booker": {
"dateOfBirth": "1990-05-15",
"passportNumber": "GB123456789",
"nationality": "GB",
"passengerType": "adult"
}
}
}
}'See also:
docs/PATH_KEYED_ATTRIBUTES_MIGRATION_GUIDE.mdfor schema configuration, migration recipes, and a comparison with the legacy commands.
CapturePerson is not deprecated — it is still the way to bind a subject_id to a
person slot before you write per-person attributes with SetAttributes.
person_ref is a journey-local slot name (e.g. "lead_booker", "passenger_0"). It is not
PII and is stored in plaintext. subject_id is a stable UUID from your identity system — reuse
it for the same person across multiple journeys so a single erasure request covers all of them.
Name, email, and phone are encrypted at rest using AES-256-GCM under a per-subject Data Encryption Key (DEK).
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"CapturePerson": {
"person_ref": "passenger_0",
"subject_id": "'"$SUBJECT_ID"'",
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "+44-7700-900000"
}
}'The commands below still work and replay correctly, but they are now annotated
#[deprecated] in the domain model (since = "0.3.0", which is not yet published).
New integrations should use SetAttributes above; see the
migration guide for a
command-by-command mapping. They remain fully functional until an explicit
removal RFC is accepted.
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"Capture": {
"step": "search",
"data": {
"search": {
"tripType": "round-trip",
"origin": "LHR",
"destination": "JFK",
"departureDate": "2025-08-15",
"passengers": {
"total": 1,
"adults": 1,
"children": 0,
"infants": 0
}
}
}
}
}'Free-form PII details for an existing person slot. Always encrypts regardless of schema.
Multiple calls for the same person_ref are merged.
curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '{
"CapturePersonDetails": {
"person_ref": "passenger_0",
"data": {
"dateOfBirth": "1990-05-15",
"passportNumber": "GB123456789",
"nationality": "GB",
"passengerType": "adult"
}
}
}'curl -X POST http://localhost:3030/journeys/{journey_id} \
-H "Content-Type: application/json" \
-d '"Complete"'curl -i -X DELETE http://localhost:3030/subjects/{subject_id}Returns 204 No Content. This:
- Permanently deletes the subject's Data Encryption Key — all ciphertext belonging to that subject in the event store becomes irrecoverable.
- Emits a
SubjectForgottenaudit event on every affected journey's event stream. - Nulls out the subject's
journey_personrow(s) and setsforgotten = true. - Leaves all other persons' data and the journey's shared (non-PII) data completely intact.
See docs/QUICK_START.md for a full walkthrough including a
crypto-shredding demo.
For guidance on how to mint or resolve subject_id values — including the additional-passenger
case where no identity-system UUID is available — see
docs/SUBJECT_ID_STRATEGIES.md.
# Unit and integration tests (requires a running Postgres)
cargo test
# Lint
cargo clippy -- --no-deps -Dclippy::pedantic -Dwarnings| Document | Description |
|---|---|
CHANGELOG.md |
What changed, including the path-keyed attributes work and the list of deprecated commands/events/fields |
docs/PATH_KEYED_ATTRIBUTES_MIGRATION_GUIDE.md |
Start here — migrating to SetAttributes / AttributesSet (path-keyed attributes) |
docs/PATH_KEYED_ATTRIBUTES_DESIGN.md |
Design proposal and rationale behind path-keyed attributes |
docs/QUICK_START.md |
Step-by-step walkthrough and crypto-shredding demo |
docs/MULTI_SUBJECT_DESIGN.md |
Multi-subject GDPR crypto-shredding design (current) |
docs/PERSON_CAPTURE.md |
CapturePerson command reference (not deprecated); legacy CapturePersonDetails reference |
docs/IMPLEMENTATION_SUMMARY.md |
What was built and why |
docs/ARCHITECTURE_REVIEW.md |
Architecture Review Board (ARB) review document |
docs/SUBJECT_ID_STRATEGIES.md |
How to mint or resolve subject_id values (authenticated users, additional passengers, GDPR erasure by email) |
# Must be run from the examples/flight-booking directory
cargo run -p flight-booking --bin generate_schema