API · Moderation
Pre-commit content moderation with @voltro/plugin-moderation — checks named input fields BEFORE the handler; a block rule rejects banned content with typed ContentRejected (write never commits), a flag rule writes it but queues it for review. keywordProvider or aiProvider. Memory store, zero infra.
Stop bad content before it's written. @voltro/plugin-moderation checks named input fields on the rpc interceptors, before the handler: a block rule rejects the call with typed ContentRejected (the write never commits); a flag rule lets the write through but queues it for review. The provider is pluggable — keywordProvider is a zero-dep deny-list, aiProvider() an optional LLM classifier. Config-only, zero infra. Template id: api-moderation.
Scaffold
voltro create-project acme --api=api-moderationRules as config
// app.config.ts
import { moderationPlugin, keywordProvider } from '@voltro/plugin-moderation'
moderationPlugin({
provider: keywordProvider(['spam', 'scam', 'phishing', 'malware']),
rules: [
{ match: 'posts.create', fields: ['title', 'body'], action: 'block' }, // reject
{ match: 'comments.create', fields: ['body'], action: 'flag' }, // write + queue
],
})There's no moderation code in the handlers — posts.create and comments.create are plain inserts; the interceptor enforces the rules.
Block vs flag
- block — banned content fails typed
ContentRejected({ tag, categories, reason }) before the write. Declareerror: ContentRejected(from@voltro/plugin-moderation/errors) on the procedure. - flag — the row IS written, but flagged; the dashboard's Moderation panel surfaces the review queue. For rewriting rather than rejecting, the in-handler
moderate(text)helper returns a verdict you act on (the interceptor can't rewrite input).
Try it
# banned word in a post → blocked, nothing written:
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d '{"tag":"posts.create","input":{"title":"hi","body":"buy cheap spam now"}}'
# → { ok:false, error:{ _tag:"ContentRejected", reason:"matched denied term(s): spam" } }
# clean post → written; banned COMMENT → written but flagged (flag doesn't block):
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d '{"tag":"comments.create","input":{"body":"this is spam"}}'
# → { ok:true, … } (queued for review)Swap in AI
Replace keywordProvider([...]) with aiProvider() — the rules don't change. aiProvider fails OPEN (a classifier outage lets content through rather than blocking everything).
Anti-patterns
- Putting the keyword list in the client. Moderation runs server-side on the interceptor — a client check is bypassable. Keep the provider + rules on the api.
- Using
blockwhere you meantflag.blockis a hard reject (nothing written);flagkeeps the content for human review. Pick per surface — comments oftenflag, public posts oftenblock.