CIME is a B2B fleet maintenance and management platform designed to help small and medium-sized companies optimize vehicle operations, reduce downtime, and improve maintenance efficiency without requiring expensive hardware or telematics.
One of the core ideas behind the platform is that field workers shouldn’t need to learn enterprise software. Drivers and mechanics already communicate through WhatsApp everyday. Instead of forcing them into structured forms, or a new application CIME wants to use natural language as the primary interface to the system.
That introduces a different engineering problem. Human conversations are ambiguous, while fleet operations require deterministic execution. A message like “the truck is making that noise again” isn’t a database operation — it needs to be interpreted, validated, associated with the correct vehicle and company, and ultimately translated into a business action.
The architecture was designed around that boundary: allowing an LLM to understand natural language while keeping business logic, authorization, and persistence inside deterministic application services.
CIME is split into four layers that hand a request off in sequence — from the people using it, through the AI and the API, down to the database.
- Clients — two entry points: a Next.js / React dashboard for managers, and WhatsApp itself for drivers and mechanics in the field, so there’s no app to install.
- AI & Integrations — WhatsApp text and voice flow through a Node + Baileys bot on Fly.io, which uses OpenAI to turn natural language into structured commands.
- Backend · Fly.io — an Express + TypeScript API with JWT / CORS middleware, REST routes, and controllers / services that hold the business logic.
- Data · Supabase — Prisma gives type-safe access to a Supabase-managed PostgreSQL database, the single source of truth for fleet and maintenance data.
Every request flows through five layers before reaching the database. The language model understands the request, but every operation is executed by deterministic backend services.
- AI Reasoning — GPT-4 reads the text or voice message, classifies the intent, plans the steps, and emits structured calls through OpenAI function calling.
- AI Orchestration — a tool registry dispatches each call to the right tool, while conversation memory feeds earlier context back into the model.
- Available Tools — a typed surface for vehicles, maintenance, work orders, inspections, and reporting: the only actions the model is allowed to take.
- Domain Layer — domain services apply authorization, tenant isolation, and business rules, so every request is scoped to the user’s company.
- Persistence — Prisma writes to PostgreSQL with type-safe queries, the same store the dashboard reads from.
The diagrams above show the components, not the execution loop. Two forks decide what a message costs: known phrasings never reach the model at all, and the ones that do fork again on what the model asked for — a read stays inside a read-only transaction, a write goes through the REST API and Prisma.
- 01
A WhatsApp text or voice note reaches the bot on Fly.io. Voice notes are transcribed by Whisper and re-enter as text.
- 02
The phone number resolves which user and company the message belongs to.
- 03
Deterministic regex handlers run first — problem reports, fleet lists, order lists, the checkup flow. Only what they do not match reaches the model.
matched — the model is never called“hose problem, plate 1234” stops here: the issue handler parses the plate and description itself, resolves the truck, and creates the order through the same REST API, then confirms on WhatsApp.no match — everything below runs“how many open orders does truck ABC1234 have?”, “how many trucks are in the fleet?”, “open a work order for ABC1234, burst hose”. Free phrasing, questions, and anything the patterns were not written for. - 04
The message, session context, and a system prompt built from the Prisma schema go to OpenAI with three function definitions.
Which function did the model call?
read
- 05
“how many open orders does truck ABC1234 have?”
run_report— the model sends a semantic plan (metric, period, filters) and the backend builds the SQL. The model never writes SQL here. - 06
“how many trucks are in the fleet?” → SELECT COUNT(*) FROM truck
run_sql— one SELECT, table allow-list, no SELECT *, company filter injected, automatic LIMIT 200. - 07
Runs in a READ ONLY transaction behind statement and lock timeouts, with an EXPLAIN cost gate. Nothing is written.
write
- 08
“open a work order for ABC1234, burst hose”
create_order— refused outright when the message reads like a question (“how many…”, “list…”, “show…”). - 09
The plate is resolved to a truck id, then POST /api/trucks/:id/maintenance-items with the scoped token.
- 10
Express validates the payload, Prisma writes, and PostgreSQL persists the maintenance order.
- 11
The function result is appended to the conversation and sent back to the model in a second call.
- 12
The model writes the reply and the bot sends it on WhatsApp. Managers see the same record in the dashboard, through the same API.
The model decides what to ask for, never how it runs: both branches end in the same deterministic services the web dashboard uses, and the reply is written only after the result comes back.
The model never gets a database connection or arbitrary backend code. It can call exactly three typed functions — one that reports, one that reads, one that writes:
run_report({
metric: "orders"
agg: "count" | "list"
time: { preset: "today" | "this_week" | "last_7d"
| "this_month" | "all" | "custom" }
group_by: "status" | "plate" | "author" | "day" | "week"
filters: { plate?, status_in?, type?, search? }
limit: 1..500
})
run_sql({
query: string // a single SELECT, nothing else
})
create_order({
plate: string
title: string
})Preferring run_report is the point: the model sends a semantic plan — metric, period, filters — and the backend composes the SQL, so the common case never has the model writing queries at all. Whichever it picks, the backend still validates the arguments, resolves the company from the authenticated phone number, injects the tenant filter, and executes.
Guardrails are the limits that define what an AI feature is able to do, separately from what the model decides to ask for. They answer three questions: what can be called, what can be read, and what can be written — and they are enforced by the application, so the answers stay the same whatever arrives in the conversation.
These are the ones I built into CIME, and what each of them prevents.
- Tool allow-list — three functions, nothing else. An unknown function name is an error, not an attempt.
- Typed schemas — parameters are JSON-schema constrained (enums for periods and groupings, 1–500 on limits) and validated before anything runs.
- Trusted tenant context — the company comes from a phone-number lookup on the authenticated identity, never from the model, and is injected into the query as a filter rather than being asked for.
- Reads are SELECT-only — writes, DDL, and multi-statement queries are rejected outright; so are
SELECT *and any table outside a four-table allow-list on the public schema. - Read-only transactions — analytical queries run inside
BEGIN READ ONLYwith statement, lock, and idle timeouts, so a read cannot mutate or block production data. - Cost gate — every query is run through
EXPLAINfirst and refused if the plan is too expensive; non-count queries get an automaticLIMIT. - Intent guard on writes — the create path is refused when the message reads like a question, so “how many orders are open?” can never open one.
- Writes go through the API — never straight to the database: the REST endpoint, its Express validation, and Prisma are the only way in, exactly as the dashboard uses them.
- Bounded responses — the whole model path is capped by a timeout and failures return a controlled message instead of hanging the conversation.