Chat SDKs own your data
Moderation that ships inside a chat SDK only sees what lives inside that SDK. To use it you migrate your messages, your schema and your auth, and your product now runs on someone else's stack.
Real-time watch layer
validAIt reads each new row through the trigger your database already has, asks Jev a few typed questions about it and writes the answers back as flags. Under half a second per decision, fractions of a cent per message. No chat SDK to adopt, and nothing is deleted unless you turn that on.
Connectors run inside your own Firebase, Supabase, Appwrite, Convex, Atlas or PocketBase project. Your TypeSafe key stays in your environment. validAIt hosts the control plane: policies, thresholds, the review queue and alerts.
Free during early access. No card, no chat migration.
Fictional messages. Decisions appear after a simulated round trip of 70 to 500 ms.
The problem
Small teams ship chat, comments and uploads on Firebase or Supabase in a weekend, then discover that every moderation option asks them to move, wait or settle for less.
Moderation that ships inside a chat SDK only sees what lives inside that SDK. To use it you migrate your messages, your schema and your auth, and your product now runs on someone else's stack.
A generative model answers in seconds and bills for every token it writes. At chat volume, teams end up sampling a slice of traffic instead of looking at all of it.
A toxicity score is fast and cheap, and it cannot tell you whether a user is trying to move a deal off-platform, or whether a comment needs a reply from your team today.
How it works
One policy file describes what to watch, what to ask and what to do. validAIt runs it on every row that lands.
Point validAIt at a collection or table through the native trigger of your platform. Map the fields it may read, for example text, authorId and roomId. Nothing else leaves your database.
Write your questions as yes or no, one of up to 255 options, or a score against a rubric. Several questions go out in one request, and the answers come back typed inside your schema.
Set thresholds per question. validAIt writes flags back to the record, notifies your team and queues items for human review. Automatic deletion is off until you turn it on.
source: firestore
collection: rooms/{roomId}/messages
fields: [text, authorId, roomId]
ask:
- id: off_platform
type: noul # yes or no with a calibrated probability
prompt: Is this message trying to move the deal off-platform?
- id: category
type: choice
options: [harassment, scam, spam, normal]
- id: urgency
type: score # continuous score against a rubric
prompt: How urgently should a human look at this, from 0 to 1?
act:
- when: off_platform.p >= 0.85
do: [flag, queue_for_review]
- when: category == scam
do: notify(slack: "#trust-and-safety")
- when: urgency >= 0.7
do: prioritizeIntegrations
Every connector uses the mechanism your platform already gives you. Nothing moves: database, schema and auth stay where they are.
// npm i firebase-functions firebase-admin
const { onDocumentCreated } = require('firebase-functions/v2/firestore');
const { initializeApp } = require('firebase-admin/app');
initializeApp();
exports.validaitOnMessage = onDocumentCreated('rooms/{roomId}/messages/{msgId}', async (event) => {
const snap = event.data;
if (!snap) return;
const res = await fetch('https://api.validait.dev/v1/events', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.VALIDAIT_KEY}` },
body: JSON.stringify({
source: 'firestore',
collection: 'messages',
id: snap.id,
fields: { text: snap.get('text'), authorId: snap.get('authorId') },
}),
});
await snap.ref.update({ validait: await res.json() });
});create extension if not exists pg_net with schema extensions;
create or replace function public.validait_on_message() returns trigger
language plpgsql security definer as $$
begin
perform net.http_post(
url := 'https://api.validait.dev/v1/events',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.validait_key', true)
),
body := jsonb_build_object(
'source', 'supabase',
'collection', tg_table_name,
'id', new.id,
'fields', jsonb_build_object('text', new.text, 'authorId', new.author_id)
),
timeout_milliseconds := 5000
);
return new;
end;
$$;
create trigger validait_messages_ai after insert on public.messages
for each row execute function public.validait_on_message();// Appwrite Function, trigger: tablesdb.*.tables.messages.rows.*.create
import { Client, TablesDB } from 'node-appwrite';
export default async ({ req, res }) => {
const row = JSON.parse(req.bodyRaw);
const r = await fetch('https://api.validait.dev/v1/events', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.VALIDAIT_KEY}` },
body: JSON.stringify({
source: 'appwrite',
collection: 'messages',
id: row.$id,
fields: { text: row.text, authorId: row.authorId },
}),
});
const client = new Client()
.setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT)
.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
.setKey(req.headers['x-appwrite-key']);
await new TablesDB(client).updateRow({
databaseId: 'main', tableId: 'messages', rowId: row.$id,
data: { validait: JSON.stringify(await r.json()) },
});
return res.empty();
};// convex/messages.ts
import { mutation, internalAction } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';
export const send = mutation({
args: { text: v.string(), authorId: v.string() },
handler: async (ctx, args) => {
const id = await ctx.db.insert('messages', args);
await ctx.scheduler.runAfter(0, internal.messages.check, { id });
},
});
export const check = internalAction({
args: { id: v.id('messages') },
handler: async (ctx, { id }) => {
const m = await ctx.runQuery(internal.messages.get, { id });
const r = await fetch('https://api.validait.dev/v1/events', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.VALIDAIT_KEY}` },
body: JSON.stringify({
source: 'convex',
collection: 'messages',
id,
fields: { text: m.text, authorId: m.authorId },
}),
});
await ctx.runMutation(internal.messages.save, { id, validait: await r.json() });
},
});exports = async function (changeEvent) {
const doc = changeEvent.fullDocument;
const r = await context.http.post({
url: 'https://api.validait.dev/v1/events',
headers: {
'Content-Type': ['application/json'],
Authorization: ['Bearer ' + context.values.get('VALIDAIT_KEY')],
},
body: JSON.stringify({
source: 'mongodb',
collection: changeEvent.ns.coll,
id: doc._id.toString(),
fields: { text: doc.text, authorId: doc.authorId },
}),
});
const validait = EJSON.parse(r.body.text());
await context.services
.get('mongodb-atlas')
.db(changeEvent.ns.db)
.collection(changeEvent.ns.coll)
.updateOne({ _id: doc._id }, { $set: { validait } });
};// pb_hooks/validait.pb.js
onRecordAfterCreateSuccess((e) => {
const res = $http.send({
url: 'https://api.validait.dev/v1/events',
method: 'POST',
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + $os.getenv('VALIDAIT_KEY') },
body: JSON.stringify({
source: 'pocketbase',
collection: e.record.collection().name,
id: e.record.id,
fields: { text: e.record.getString('text'), authorId: e.record.getString('authorId') },
}),
timeout: 10,
});
e.record.set('validait', res.json);
e.app.save(e.record);
e.next();
}, 'messages');curl -X POST https://api.validait.dev/v1/events \
-H 'content-type: application/json' \
-H "authorization: Bearer $VALIDAIT_KEY" \
-d '{
"source": "my-api",
"collection": "messages",
"id": "msg_123",
"fields": {
"text": "hey, lets finish this on whatsapp",
"authorId": "u_42",
"roomId": "r_7"
}
}'Use cases
Marketplaces, edtech, dating apps, communities and forums put different questions to the same kind of rows.
Catch the things a toxicity score cannot see, while the conversation is still open.
Keep a public feed readable without a full time moderator.
We describe the image with a vision model first, then Jev decides.
Under the hood
validAIt runs on TypeSafe Jev, a decision model that answers inside a schema instead of writing prose.
{
"state": "Hey, WhatsApp me at +34 6xx xxx xxx and we settle it there",
"questions": [
{ "id": "off_platform", "type": "noul",
"prompt": "Is this message trying to move the deal off-platform?" },
{ "id": "category", "type": "choice",
"options": ["harassment", "scam", "spam", "normal"] },
{ "id": "urgency", "type": "score",
"prompt": "How urgently should a human look at this, from 0 to 1?" }
]
}Independence. Jev is a TypeSafe AI product. validAIt is the watch layer built on top, and is not affiliated with TypeSafe AI.
Trust and control
Every decision is visible, and a person can reverse any of them.
Pricing
Paid plans for the validAIt layer will be announced before general availability. Model spend is billed to your own TypeSafe account.
Free
Free during early access.
Tell us where your messages live. We reply with a connector and a policy to try on your own data.
FAQ
No. validAIt reads through the native trigger of the platform you already run, and writes its decisions back into your own records. There is no validAIt SDK in your client and no migration of your messages.
You get typed answers with calibrated probabilities, so you set the threshold. Below it, nothing happens. Above it, the item lands in the review queue with its answer and probability, and a person decides. Nothing is deleted by default, so a wrong call is a flag someone clears.
Only the fields you map, for example text, authorId and roomId. Nothing else leaves your database. Your connector sends those fields to TypeSafe from your own environment, and the decisions to the review queue. TypeSafe processes model requests in the United States, so check their DPA before mapping a field you consider sensitive.
In Beta. We describe the image with a vision model first, then Jev decides on that description. Jev is text only and does not read images or audio, so the description step is what makes media work.
English is strongest today. TypeSafe says other languages are handled but not as accurately, so run your own Spanish or other-language messages through it before you trust a threshold. We help you set up that test.
The connectors already run inside your own infrastructure. A self-hosted control plane is planned.
In your own environment. Connectors run inside your Firebase, Supabase, Appwrite, Convex, Atlas or PocketBase project, and the key sits in that project's secrets or function environment, never on our servers. validAIt hosts the control plane only. Model requests are billed to your own TypeSafe account at $0.042 per million input tokens, output free.
A decision model from TypeSafe AI, launched on 15 September 2026. It answers typed questions, yes or no with a probability, one of up to 255 options, or a score against a rubric, in 70 to 500 ms end to end, and it does not generate free text. validAIt is built on it and is not affiliated with TypeSafe AI.