DEV Community

Kate Johnson
Kate Johnson

Posted on

The tenant-safety checklist for an AI endpoint

Adding an LLM call to an existing SaaS product can look deceptively small: fetch a document, send
it to a model, and return a summary. In a multi-tenant system, that endpoint crosses several trust
boundaries at once. A feature that works in a demo can still expose the wrong tenant's data, accept
an unauthorized document ID, or save output under the wrong account.

I use this checklist before treating an AI endpoint as production-ready.

1. Derive tenant scope from the authenticated request

Never accept tenantId from the request body as proof of scope. Resolve it from the authenticated
user or service identity, then carry that trusted scope into every query.

const document = await prisma.document.findFirst({
  where: {
    id: input.documentId,
    tenantId: auth.tenantId,
  },
});

if (!document) {
  throw new NotFoundException();
}
Enter fullscreen mode Exit fullscreen mode

Returning 404 for both a missing and an unauthorized document also avoids confirming that a
record exists in another tenant.

2. Apply authorization before the model call

The model is not an authorization layer. It should only receive data the caller has already been
allowed to access. Check tenant scope, user role, document state, and field-level restrictions
before building the prompt.

This order matters because logging and retry systems may retain the model request. If restricted
data reaches the provider even once, rejecting the response afterward is too late.

3. Minimize the prompt payload

Send the fields needed for the task, not the whole database record. Exclude internal notes,
credentials, unrelated customer data, and metadata that cannot affect the answer.

A small prompt is cheaper and easier to audit. It also reduces the damage if a provider log or
debug trace is exposed.

4. Treat structured output as untrusted input

JSON mode improves formatting. It does not make the values correct.

Validate the response with a schema, enforce length and enum limits, and reject unknown fields.

const SummarySchema = z.object({
  summary: z.string().min(1).max(2_000),
  category: z.enum(["billing", "support", "sales", "other"]),
  confidence: z.number().min(0).max(1),
});

const result = SummarySchema.parse(modelResponse);
Enter fullscreen mode Exit fullscreen mode

If the output will trigger another action, add a deterministic policy check after validation.

5. Store tenant context with the result

An AI result should not become a free-floating artifact. Persist the tenant, source record,
model, prompt version, and request identity with it. That makes later reads safe and gives support
teams enough context to investigate a bad result.

6. Test the negative paths

The most valuable tests are not the happy-path summary. They prove that:

  • a user from tenant A cannot summarize tenant B's document;
  • an allowed user cannot request a disallowed document state;
  • malformed model output is rejected;
  • retries do not create duplicate records;
  • logs do not contain the raw sensitive payload;
  • a timeout fails safely without leaving partial state.

For row-level security, run at least one integration test through the same database role used in
production. A mocked repository cannot prove that the policy is correct.

7. Make the feature observable by tenant

Track latency, provider errors, validation failures, token usage, and cost with tenant-safe labels.
Avoid putting customer text in metrics or error messages. An operator should be able to answer,
"Is this failing for one tenant or for everyone?" without opening raw prompts.

The short version

An AI endpoint is still an application endpoint. Authentication, tenant scope, authorization,
data minimization, validation, persistence, tests, and observability all come before trusting the
model's answer.

The model call may be the newest line in the architecture, but the oldest security rules still
apply.

Top comments (0)