Security controls
This page is written for security teams and auditors who are to assess Revizo AI. It describes each control in the order it runs, which threats it addresses, and what we deliberately do not claim.
Control chain
Every chat request passes these controls. A request that is stopped early never reaches the model.
| # | Control | Implementation | Threat addressed |
|---|---|---|---|
| 1 | Authentication | withTenant reads the Clerk session server-side. tenantId and userId cannot be set by the client. | Unauthenticated use, tenant spoofing via parameters |
| 2 | Role | Chat is a write endpoint (POST). The Viewer role (org:viewer) is rejected. | Read-only users performing actions |
| 3 | AI policy | Fail-closed: unknown organisation or missing setting is treated as declined. Checked before anything is sent to a provider. | Processing the organisation has opted out of |
| 4 | Monthly quota | Administrator can set a cap on tokens and number of chats per month. | Cost runaway, abuse |
| 5 | Rate limit | 10 chat requests per minute per user, in addition to the global limit. | Automated abuse, large-scale extraction |
| 6 | Input validation | Messages, images (max 4, type-limited, size-limited), conversation ID, task ID, and company ID are validated. Invalid company ID is discarded silently instead of failing. | Injection via parameters, DoS with large payloads |
| 7 | Classification | Rule-based classification. Obviously off-topic (code, poems, politics, general facts) is rejected without a model call. | Abuse of the model as a general service, cost |
| 8 | Context | Every lookup has tenant_id = the session organisation. Client and company names are fetched only when the object belongs to the organisation; otherwise they are omitted. Task context uses the same visibility rule as the rest of the app. | Cross-tenant leakage via context, enumeration of others’ objects |
| 9 | Tool authorisation | Each handler receives orgId and userId from the session and filters on them. The model’s arguments are proposals; the handler verifies ownership (for example validateClientTenant before Smart Match). | The model is manipulated into asking for foreign data |
| 10 | Card validation | Cards with invalid form are discarded. Max one choice card per reply. The card’s payload is removed from what the model gets to see. | Half-finished or forged actions, leakage via tool result |
| 11 | Reply filter | Blocked patterns (advice wording, national ID numbers, bank account numbers) are replaced. Required disclaimers are added for deadlines, tax rules, and legal quotations. Leaked tool syntax is stripped. | Unintended advice, PII in replies |
| 12 | Storage and audit trail | Conversation, tools used, and usage are stored in the organisation’s tenant. Writing tools log to the audit log. | Missing traceability |
Threat model
Prompt injection
Prompt injection is an attempt to get the model to break the instruction — via the user’s own text, via content in a screenshot, or via data that comes back from a tool (for example a task title written by someone else).
Our position is that the instruction to the model is not a security boundary. We assume the model can be manipulated, and design so that it does not give the attacker anything:
| If the model is manipulated into … | What happens |
|---|---|
| asking for data from another organisation | The tool filters on the session’s tenant_id. Empty result. |
| asking for a task the user cannot see | The visibility filter in the tool returns nothing. |
| sending email to a free address | A documentation request requires a card and a click. send_message_to_contact requires that the recipient exists in the organisation’s contact register. |
| saving a routine description | The card requires a click. HTML is sanitised with DOMPurify before it is shown and saved. |
| executing a tool that does not exist | Dispatcher returns “Unknown tool”. |
| calling the same writing tool in a loop | Max 5 tool rounds per request. |
| reproducing a national ID number | The reply filter replaces the pattern. The tools do not return the field. |
| writing fake tool syntax in the reply | Stripped before display. The model cannot “pretend” it has executed something — only actual tool results count. |
The remaining attack surface is actions the user themselves is entitled to perform, performed on the wrong object because the model misunderstood. That is why writing in Revizo (tasks, matching) is logged with who, what, and when, and can be reversed in the ordinary way.
Cross-tenant leakage
Primary protection is the application layer: tenant_id from the session in every query. This is the same mechanism as the rest of Revizo, and the AI tools use the same database helpers. Row Level Security in the database is a secondary safety net for the Supabase Data API, not primary protection for the application’s own queries. See Data processing and storage → Data segregation.
Unintended advice
Accounting and tax advice from a language model is a professional and liability risk. We address it in three layers: the instruction forbids it, the reply filter catches typical wording and adds disclaimers, and legal lookups go against Lovdata with a source citation instead of the model’s memory. None of the layers is perfect; the user is informed that the assistant does not give advice.
Cost runaway and availability
Monthly quota per organisation, rate limit per user, max 5 tool rounds, max reply length, and 60-second timeout. Model calls have a limited number of retries on provider errors. AI chat is not critical for Revizo; if the provider goes down, the rest of the platform works.
Audit trail
| Event | Logged as | Where |
|---|---|---|
| Task created or changed via assistant | task.created, task.updated | The organisation’s audit log |
| Smart Match run via assistant | match.created | Audit log |
| Email sent to a contact via assistant | agent.message_sent with recipient | Audit log |
| Reminder created or deleted | reminder.created, reminder.deleted | Audit log |
| Export generated | export.generated | Audit log |
| Tripletex writing | tripletex.voucher.created, tripletex.invoice.created, tripletex.supplier_invoice.approved | Audit log |
| Each model call | Model, tokens, category, user, tools used | ai_usage_log (per tenant) |
| Each conversation | Messages, replies, tools used, page context | ai_conversations (per tenant, 90 days) |
The audit log is protected against change and deletion at database level. See Security architecture.
Administrator controls
| Control | Where | Effect |
|---|---|---|
| Turn off all AI | Settings → AI & Privacy → master switch | Chat, document analysis, and voice are hidden in the UI and rejected on the server |
| Turn off only chat, document analysis, or voice | Same page, individual switches | Only that function is affected |
| Set monthly quota | Same page | Requests over the quota are rejected with 429 |
| Confirmation dialog before turning off | Automatic | Shows what disappears and what is kept |
| Role restriction | User administration | Viewers cannot use chat |
| Remove Lovdata or Kundetiltak | Plan and marketplace | Associated tools are removed from the catalogue |
Only administrators can change AI settings.
Limitations we are open about
A security review is not improved by documentation promising more than the system does. The following is true today:
- Instructions and regex are not authorisation. System prompt and reply filter reduce the risk of unwanted content, but it is the tools’ tenant filter and the confirmation cards that are the security boundaries. We describe them that way.
- Two email tools are confirmed in the conversation, not with a card.
send_message_to_contact(recipient must be a registered contact) andsend_report_email(default recipient is the user themselves). Both are logged. Cards for these are planned. - Accounting writes via the Tripletex tools are executed when the user asks for them, without a dedicated confirmation card. The integration is optional, and the actions are logged.
- Screenshots the user pastes are sent to Anthropic as an image. The user themselves controls what is pasted.
- AI processing happens outside the EEA (Anthropic and OpenAI in the USA) under EU standard contractual clauses. Organisations that do not accept that can turn AI off; the rest of Revizo is unaffected.
- The rate limit is per user, not per organisation. The monthly quota is per organisation.
- We are working on one shared policy control for all AI data flows, so that the organisation’s AI setting is enforced identically in chat, document analysis, voice, and background jobs. Chat, document analysis, and voice are covered today.
If you want a review against a concrete questionnaire (CAIQ, SIG, your own template), see Questions and answers for due diligence or get in touch.
Last updated: September 2026