Flows and guardrails
The book-sharing system relies on Directus flows to keep user-generated interactions honest.
That may sound heavy, but it is really a practical response to how social features fail when the backend trusts the client too much.
Without these flows, a client could try to:
- assign content to another user
- comment on private or unpublished targets
- change the target of an existing comment or reaction
- create duplicate reactions for the same target
We use flows because these are business rules, not just form rules. They depend on the current user, the current target, and the state of other records.
Active flows
| Flow name | Purpose |
|---|---|
[Book Sharing] Validate Reading Share Comment Create |
Validates share comments on create and assigns owner |
[Book Sharing] Lock Reading Share Comment Ownership |
Preserves user and reading_share on update |
[Book Sharing] Validate Reading Share Reaction Create |
Validates share reactions, assigns owner, blocks duplicates |
[Book Sharing] Lock Reading Share Reaction Ownership |
Preserves user and reading_share on update |
[Book Sharing] Validate Reading Journey Comment Create |
Validates journey comments on create and assigns owner |
[Book Sharing] Lock Reading Journey Comment Ownership |
Preserves user and reading_journey on update |
[Book Sharing] Validate Reading Journey Reaction Create |
Validates journey reactions, assigns owner, blocks duplicates |
[Book Sharing] Lock Reading Journey Reaction Ownership |
Preserves user and reading_journey on update |
[Book Sharing] Validate Book Comment Create |
Validates book comments on create and assigns owner |
[Book Sharing] Lock Book Comment Ownership |
Preserves user and book on update |
[Book Sharing] Validate Book Reaction Create |
Validates book reactions, assigns owner, blocks duplicates |
[Book Sharing] Lock Book Reaction Ownership |
Preserves user and book on update |
The naming is intentionally repetitive. Each flow is small and collection-specific, which makes it easier to debug than one large shared automation with many branches.
Create-flow pattern
Each create-validation flow follows the same basic idea.
- Require an authenticated user.
- Check that the target foreign key is present.
- Read the target record with enough permission to validate it.
- Reject the write if the target is missing or not interactable.
- Overwrite
userfrom the current accountability context. - For reactions, check whether the same
(target, user)pair already exists.
This pattern exists so the system can accept simple client requests while still enforcing server-side truth.
- The client only needs to say what target it is interacting with.
- The backend decides whether that interaction is allowed.
- The backend also decides who the acting user really is.
Update-lock pattern
Each update flow protects two things:
- the original owner field
- the original target foreign key
This stops clients from moving a comment or reaction to a different target after creation, and it stops them from reassigning ownership.
That matters because update endpoints are often where data models quietly drift. A create flow alone is not enough if later updates can rewrite the important foreign keys.
Write path diagram
flowchart TD
A[Client sends create request] --> B{Authenticated user present?}
B -- No --> X[Reject write]
B -- Yes --> C{Target id present?}
C -- No --> X
C -- Yes --> D[Read target record]
D --> E{Target is visible and interactable?}
E -- No --> X
E -- Yes --> F[Overwrite owner field from accountability]
F --> G{Reaction collection?}
G -- No --> H[Allow create]
G -- Yes --> I{Duplicate target-user pair exists?}
I -- Yes --> X
I -- No --> H
Enforcement matrix
| Rule | Comments | Reactions |
|---|---|---|
| Require authenticated user | Yes | Yes |
| Require target foreign key | Yes | Yes |
| Validate target state | Yes | Yes |
| Overwrite owner field | Yes | Yes |
| Preserve target on update | Yes | Yes |
| Preserve owner on update | Yes | Yes |
| Block duplicate target-user pair | No | Yes |
The comment and reaction rules are similar on purpose, but not identical.
- Comments are expressive content, so the main concern is valid target plus correct ownership.
- Reactions are binary signals, so duplicate prevention becomes part of the model itself.
Why flows instead of schema alone
Directus field settings can handle required fields and dropdown values, but they cannot reliably answer questions like:
- Is this share public and published?
- Is this journey public and still active enough for interaction?
- Has this user already reacted to this target?
- Did the client try to spoof the owner field?
Those checks depend on other records or on the authenticated user context, so flows are the right place for them.
This is the core reason for the design. Schema rules are great for shape. Flows are better for meaning.
Known gap
The biggest remaining gap is not on write. It is on read.
reading_share_comments.visibility = share_author_only still needs application-side filtering when comments are listed back to users. The flows protect create and update rules, but they do not replace that read-path logic.
We kept that gap explicit in the docs because it is easy for teams to assume that write enforcement automatically solves read visibility. It does not. The system is safer when that boundary is spelled out clearly.