The dropdown was listing "connections" — a row per adapter + model + key — which made choosing a model an act of configuration, put a key icon and a bin inside a menu item, and showed five accounts nobody had created. A provider is now a vendor Officer curates. Every one is listed, set up or not, because a vendor you have no key for is exactly what you need to see in order to add one. The model is chosen per generation, from that provider's entry in the catalogue, beside it. With the model off the row, all that remained of a "provider" was the key for OpenAI — and Officer already stores keys: one active value per purpose, 0600 inside a 0700 directory, outside the backup tarball, created on first use so nothing needs provisioning. So `atelier_providers` is dropped, along with `generations.provider_id` that pointed into it. `provider` and `model` stay as text on each generation, which were always the historical fact rather than a pointer to configuration. `getSecret`, never `getKey`: this is a foreign credential, and getKey mints a random value for a purpose it cannot find — which for someone else's API key means authenticating with something invented. Resolution follows: a request names a model, and the catalogue says whose it is, rather than a database being asked which stored row served it. A model nobody in the catalogue claims is still accepted — the catalogue is advisory, and a model released today should work before we ship a file that mentions it. Two routes replace five: PUT and DELETE on /providers/:name/key. Deleting a key that came from the environment is refused rather than faked — it belongs to the install, not to this screen.
Atelier
Image generation for an Officer server: a provider-shaped API over OpenAI's image endpoints, storing what it makes so every image has a durable reference — a gallery you can browse, collections you can organise, and a job an agent can poll rather than block on.
Atelier was a standalone service before this plugin existed
(~/officer-dev/dockers/officer-atelier, still running as officer-atelier, still what the mobile app
talks to). This plugin is a fresh extraction into Officer, not a replacement for it yet — see
PLUGIN.md before assuming the two are interchangeable.
What you get
- Generate — a prompt, a model, size/quality/background, and a job you watch settle.
nimages becomenindependently-tracked rows, so one failure out of four does not discard the three that worked. - Gallery — everything generated, newest first, with a lightbox: pan, zoom about the cursor, checkerboard/dark/light backdrops for telling a transparent background from an opaque one that only looks it.
- Collections — named sets of images. A generation request can name one by name and it is created on first use; the gallery and the lightbox can file an image into one after the fact.
- A provider-shaped backend. "OpenAI" is code, written once; a provider is a row — which model, whose key, on or off — and several can share one adapter. Today OpenAI is the only adapter; the point of the shape is that a second one is a new file, not a redesign.
- An OpenAI-compatible job API, for agents.
POST /api/atelier/images/generationsaccepts OpenAI's request body verbatim and answers202with a job id rather than blocking for the length of the generation — the caller pollsGET /api/atelier/images/generations/:id, optionally with?wait=to long-poll instead of retrying in a loop.
Requirements
- An Officer server, version 1.0.0 or later.
- A separate Postgres database, and its own environment variable:
OFFICERDEV_POSTGRES_URL. Atelier's tables (generations,collections,collection_items,providers) do not live inofficer_db— seePLUGIN.mdfor why, and set this before installing. There is no default. - An OpenAI API key, set with
bun run secret:set atelier-openai-key, or configured per-provider in Settings → Integrations → Atelier, or (for an install that predates the secret store) asOPENAI_API_KEYin.env.
Install
From the Officer marketplace: find Atelier, install. That mounts /api/atelier and /atelier, starts
the officer-atelier sidecar under PM2, and connects it to whatever OFFICERDEV_POSTGRES_URL points at —
which has to already have the schema (the standalone repo's own bun db:push creates it; this plugin
does not run migrations against a database it does not own the schema of).
Uninstalling stops the sidecar and unmounts the route. It never touches your database — Atelier's tables and rows are not this plugin's to drop, since it does not own the schema push for them either.
Permissions
One permission, atelier, grantable per role at read or write.
- Read lets an account use Generate, browse the Gallery and Collections, and see which providers are configured (never a key, in any form — not even masked).
- Write additionally lets an account add, edit or delete providers, including their API keys. A provider's key authorises real spend on someone's OpenAI account, so treat write here the way you would treat write on any billing-adjacent surface.
Providers is not a panel — it is a platform Settings screen (Settings → Integrations → Atelier), because
it is owner-level configuration rather than a working view. See PLUGIN.md for why.
Limitations, honestly
- Two Ateliers can exist at once. This plugin and the standalone service are independent — installing
this one does not migrate the standalone one's data, stop it, or repoint the mobile app. See
PLUGIN.md. - No per-user data. Every account with
readsees the same gallery and the same collections — there is no ownership model yet, the same simple shape offscale shipped with first. - The database is separate from
officer_db, which means Atelier's tables are not covered bybun db:push, are not part of the platform's usual backup story unless you back up the second database too, and need their own connection string. Migrating them in is real, tracked follow-up work — seePLUGIN.md.