Moderation

Moderate user content before it commits — keyword denylist or AI provider, block or flag via rpc interceptor, plus an in-handler redact helper.

@voltro/plugin-moderation screens user-submitted content. A rule matches an rpc tag + fields; the configured provider classifies the text; the interceptor blocks (typed ContentRejected, the write never commits) or flags (annotates, lets it through). Because the interceptor can't rewrite input, redact is an in-handler helper.

Wiring

// app.config.ts
import { moderationPlugin, keywordProvider, aiProvider } from '@voltro/plugin-moderation'

export default {
  type: 'api' as const,
  name: 'api',
  plugins: [
    moderationPlugin({
      provider: keywordProvider(['spam', 'scam', 'http://badsite']),  // or aiProvider() — uses @voltro/ai
      rules: [
        { match: 'posts.create', fields: ['title', 'body'], action: 'block' },
        { match: /^comments\./, fields: ['text'], action: 'flag' },
      ],
    }),
  ],
}

Providers

  • keywordProvider(terms) — pure denylist; zero deps, deterministic, instant. Best for a known set of banned terms / URLs.
  • aiProvider(opts?) — lazy @voltro/ai classification (@voltro/ai is an optional dep). Fails open: a provider error resolves to CLEAN so an AI outage can't block all writes. Use for nuanced toxicity/abuse detection.
  • Custom — any function with the provider signature (text) => Effect.Effect<ModerationVerdict> plugs into the same provider slot. The verdict is { flagged, categories?, score?, reason? }.

Block vs flag vs redact

  • block — interceptor fails the call with typed ContentRejected ({ tag, categories, reason }, merged into the wire-error union). The write never commits.
  • flag — lets the write through; surfaces the classification for downstream review (e.g. a moderation queue).
  • Redact — the interceptor can't mutate input, so there is no redact action. Instead, call the in-handler moderate(text) helper: it classifies the text against the configured provider (returning a ModerationVerdict — it does NOT return redacted text) and the handler decides what to store:
import { moderate } from '@voltro/plugin-moderation'

export default (input: { body: string }, ctx) => Effect.gen(function* () {
  const verdict = yield* moderate(input.body)   // classifies via the configured provider
  const body = verdict.flagged ? '[redacted]' : input.body
  return yield* Effect.tryPromise(() => ctx.store.insert('posts', { body }))
})

Dashboard panel

Both dashboards ship a Moderation panel (api apps): block/flag counts + the review queue of flagged content (tag, categories, reason, subject), each pending item with Confirm (real violation) / Dismiss (false positive) actions. Resolving gates on the canModerateContent capability. Backed by /_voltro/inspect/plugins/moderation/{flagged,resolve}.

Permissions

rpc:intercept:mutation + rpc:intercept:action (moderation gates both writes and external-I/O actions) + inspect:read (dashboard panel).