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.
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:
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.
Now each tenant brings its own key. Quota is split per tenant, so one customer's imports can no longer starve the others.
Each tenant keeps its own Gemini key, encrypted at rest — the same way tenant email passwords are already stored.
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.
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.
Document import reads only the tenant's own key, looked up fresh on every request.
If a tenant has no key, import stops with a clear error instead of quietly using the shared key.
One shared global key. Read once when the service started. Same key for every tenant, so any tenant could drain the quota for all.
Each tenant's own key, decrypted at request time. No key means the import stops. Quota stays inside each tenant.
When an instructor uploads a document, the backend looks up the current tenant's key before doing any real work.
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.
Real UI captured against a local full build of this branch, at 1440×900.
The tenant's own admin manages the key here. Empty first, then after saving: the status flips to configured, the input clears, and a Remove key button appears.
A new AI settings entry was added to the admin sidebar.
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.
Operators manage any tenant's key from the tenant Features tab. Empty first, then with the key configured badge and a Remove action after saving.
What must be true so document import keeps working after this ships.
The main files behind this change.