Per-tenant Gemini key for DOCX import

Each tenant now brings its own Gemini API key for turning documents into quiz questions, stored encrypted, with no shared fallback. This page walks through why we made the change and how it behaves.

Status as of 12 Sep 2026

Built on branch feat/tenant-gemini-key. Changes are uncommitted, there is no PR yet, and nothing is on beta or prod.

Verification passed end to end:

Backend unit tests Frontend tests + build Platform-admin lint + build

Why we did this

The LMS lets an instructor upload a document and turns it into quiz questions using Google's Gemini model. Calling Gemini needs an API key, and every key has a monthly usage budget, or quota.

The problem Every tenant's document import shared one global Gemini key. One busy tenant could burn the whole quota, and then import broke for everyone. There was no way to split usage per customer.

Now each tenant brings its own key. Quota is split per tenant, so one customer's imports can no longer starve the others.

What changed, at a glance

Storage

Per-tenant key

Each tenant keeps its own Gemini key, encrypted at rest — the same way tenant email passwords are already stored.

LMS

Admin settings page

A tenant's own admin adds, replaces, or removes the key from a new AI settings page. The key field shows the saved key's prefix followed by asterisks when a key is set, and the expected shape when none is.

Platform

Operator card

The platform operator can manage any tenant's key from a card in the tenant's Features tab. Its key field uses the same hint: the saved key's prefix plus asterisks when set, the expected shape when not.

Import

Tenant key only

Document import reads only the tenant's own key, looked up fresh on every request.

Safety

No global fallback

If a tenant has no key, import stops with a clear error instead of quietly using the shared key.

Before

One shared global key. Read once when the service started. Same key for every tenant, so any tenant could drain the quota for all.

After

Each tenant's own key, decrypted at request time. No key means the import stops. Quota stays inside each tenant.

How it works

When an instructor uploads a document, the backend looks up the current tenant's key before doing any real work.

The error, and what it means

A tenant with no usable key gets a 412 · gemini_api_key_missing back. In plain terms: the import is blocked because this tenant has not set up a key yet. The UI turns it into a message telling the user to ask their admin to add one. No import job is started.

The API never returns the key itself — it only answers whether a key is configured and, at most, the key's first few characters so the UI can hint at the saved value. The secret stays inside the backend.

Screenshots

Real UI captured against a local full build of this branch, at 1440×900.

LMS admin — AI settings

The tenant's own admin manages the key here. Empty first, then after saving: the badge next to the field label flips to “Configured”, the input clears, and a Remove key button appears.

Admin AI settings page with a Not configured badge
A “Not configured” badge sits next to the field label; the placeholder shows the expected shape, “Key should look like AIza…”.
Admin AI settings page, key saved
Key saved: a green “Configured” badge next to the field label, Remove key shown, and the cleared field now shows the saved key's prefix followed by asterisks.

A new AI settings entry was added to the admin sidebar.

Admin sidebar rail with the AI settings entry highlighted
Sidebar rail — "AI settings" highlighted.

Quiz import blocked when no key is set

With the key removed, uploading a file in the quiz importer returns the missing-key error. The UI maps it to a message pointing back to the AI settings page.

Quiz question editor showing the missing Gemini key error
The missing-key error in the quiz editor.

Platform admin — AI settings card

Operators manage any tenant's key from the tenant Features tab. Empty first, then with a “Configured” badge next to the field label and a Remove action after saving.

Platform admin AI settings card with a Not configured badge
A “Not configured” badge sits next to the field label; the placeholder shows the expected shape, “Key should look like AIza…”.
Platform admin AI settings card with a Configured badge
A green “Configured” badge next to the field label and Remove key, with the field showing the saved key's prefix followed by asterisks.

Operator checklist

What must be true so document import keeps working after this ships.

Where to look

The main files behind this change.