diff --git a/PLUGIN.md b/PLUGIN.md index 736a0ae..3e1f86c 100644 --- a/PLUGIN.md +++ b/PLUGIN.md @@ -56,7 +56,7 @@ liveness, matching every other HTTP sidecar in this codebase. Decision 3 said to check whether `collections` and `provider_settings` were empty before folding them into `officer_db`. Both had rows (2 and 1), so the `atelier` Postgres database stays separate — the -sidecar keeps its own connection (`sidecar/db/db.ts`), reading `ATELIER_POSTGRES_URL` from the platform +sidecar keeps its own connection (`sidecar/db/db.ts`), reading `OFFICERDEV_POSTGRES_URL` from the platform `.env`. **This is new territory for the plugin system**: `db/schema.ts` at a plugin's root is the `officer_db`-prefixed-tables convention (`pushPluginSchema`, `_` prefix), and this plugin has no such file — `discoverPlugins()` correctly reports `schema: null` for it, and the install flow's schema @@ -65,7 +65,7 @@ step is simply skipped. Nothing in `src/servers/plugins/` needed to change for t self-managed database is indistinguishable from a plugin with no tables at all, as far as the platform is concerned. Worth knowing before assuming every plugin's data lives in `officer_db`. -**Not set automatically.** `ATELIER_POSTGRES_URL` has no default — see `BLOCKERS.md` for why (mainly: +**Not set automatically.** `OFFICERDEV_POSTGRES_URL` has no default — see `BLOCKERS.md` for why (mainly: pointing it at the live production `atelier` database while that database's own standalone service is still running risks two processes racing the same generation queue). The owner has to add it before installing. @@ -168,7 +168,7 @@ expected, not a bug. 1. **Publish to `gitea.officer.dev/plugins/atelier` and register it in the marketplace.** Not done — see `BLOCKERS.md` for why and the exact commands. -2. **`ATELIER_POSTGRES_URL` has to be set before install.** No default, on purpose. +2. **`OFFICERDEV_POSTGRES_URL` has to be set before install.** No default, on purpose. 3. **The `atelier` → `officer_db` migration**, described above, once someone can watch it happen. 4. **`assets/icon.png` and `assets/splash.png` are `[placeholder]`** flat swatches — see `assets/PLACEHOLDER.md`. @@ -176,3 +176,26 @@ expected, not a bug. touch the owner's live session or shared database. 6. **Deciding whether and when to cut the mobile app over** from `atelier.officer.dev` to the plugin's route, and what happens to the standalone service afterward. + +## Reversed 2026-08-20: officer_db, not a separate database + +The extraction kept Atelier's own `atelier` Postgres database, because `BLOCKERS.md` decision 3 said to +when the tables already had rows. The owner reversed that afterwards, and the reasoning is worth keeping: + +**A plugin that behaves differently from every other plugin is a plugin nobody can reason about.** The +separate database meant a second connection string to set before install, a second thing to back up, a +second thing to get wrong on a new machine, and failure modes no other plugin shares. None of that was +bought by anything except avoiding a migration. + +So: four `atelier_`-prefixed tables in `officer_db` — `atelier_collections`, `atelier_collection_items`, +`atelier_providers`, `atelier_generations` — created empty by `bun db:push`, like every other plugin. +Index and check constraint names carry the prefix too; those are global in Postgres and would otherwise +be the one thing that collides. + +`ATELIER_POSTGRES_URL` is gone. `sidecar/db/db.ts` reads `OFFICERDEV_POSTGRES_URL ?? POSTGRES_URL`, the +house convention, which the platform already sets. + +**The standalone service's data is NOT here.** It is still in the old `atelier` database, and the old +service is still running against it, untouched. Migrating those rows — 2 collections, 73 items, 5 +providers, 87 generations — is a deliberate, watched step, done after the plugin is installed and proven +on alpha. Nothing about this plugin depends on it happening. diff --git a/README.md b/README.md index 6ed195e..63015dc 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ talks to). This plugin is a fresh extraction into Officer, not a replacement for ## Requirements - An Officer server, version 1.0.0 or later. -- A **separate Postgres database**, and its own environment variable: `ATELIER_POSTGRES_URL`. Atelier's +- A **separate Postgres database**, and its own environment variable: `OFFICERDEV_POSTGRES_URL`. Atelier's tables (`generations`, `collections`, `collection_items`, `providers`) do not live in `officer_db` — see `PLUGIN.md` for 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 @@ -40,7 +40,7 @@ talks to). This plugin is a fresh extraction into Officer, not a replacement for ## 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 `ATELIER_POSTGRES_URL` points at — +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). diff --git a/db/schema.ts b/db/schema.ts new file mode 100644 index 0000000..32cff99 --- /dev/null +++ b/db/schema.ts @@ -0,0 +1,15 @@ +// The plugin's tables, where the platform looks for them. +// +// `scripts/gen-plugin-schemas.ts` writes `plugin-schemas.gen.ts` from every plugin's `db/schema.ts`, and +// that generated barrel is what `bun db:push` reads. So this file exists to be FOUND — the definitions +// themselves live beside the code that queries them, in `sidecar/db/`, and are re-exported here rather +// than duplicated. +// +// Everything is `atelier_`-prefixed and lives in officer_db, exactly like every other plugin. The +// extraction originally kept Atelier's own `atelier` database, on the reasoning that its tables already +// had rows. That was reversed on 2026-08-20: the plugin starts EMPTY here, and the standalone service's +// data is migrated deliberately afterwards, with someone watching. A plugin that behaves differently +// from every other plugin is a plugin nobody can reason about. +export * from '../sidecar/db/collections/schema'; +export * from '../sidecar/db/generations/schema'; +export * from '../sidecar/db/providers/schema'; diff --git a/sidecar/db/collections/schema.ts b/sidecar/db/collections/schema.ts index 23e3d16..e83ffbd 100644 --- a/sidecar/db/collections/schema.ts +++ b/sidecar/db/collections/schema.ts @@ -8,7 +8,7 @@ import { generations } from '../generations/schema'; * collection by name and creates it when it does not exist, so without the constraint a second * request with the same name would quietly start a second collection and split the batch in two. */ -export const collections = pgTable('collections', { +export const collections = pgTable('atelier_collections', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull().unique(), description: text('description'), @@ -27,8 +27,7 @@ export const collections = pgTable('collections', { * is a way of looking at generations, not a container that owns them. Deleting a generation removes * its memberships, since a row pointing at nothing is only a way to crash a join later. */ -export const collectionItems = pgTable( - 'collection_items', +export const collectionItems = pgTable('atelier_collection_items', { collectionId: uuid('collection_id') .notNull() @@ -41,6 +40,6 @@ export const collectionItems = pgTable( (table) => [ // The composite key is what makes adding an image twice a no-op instead of a duplicate. primaryKey({ columns: [table.collectionId, table.generationId] }), - index('collection_items_generation_idx').on(table.generationId), + index('atelier_collection_items_generation_idx').on(table.generationId), ], ); diff --git a/sidecar/db/db.ts b/sidecar/db/db.ts index b36a21c..f9de00b 100644 --- a/sidecar/db/db.ts +++ b/sidecar/db/db.ts @@ -2,22 +2,22 @@ import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import * as schema from './schema'; -// The `atelier` Postgres database — separate from officer_db, on purpose. See PLUGIN.md. +// officer_db, like every other plugin. // -// `ATELIER_POSTGRES_URL` rather than officer's own `OFFICERDEV_POSTGRES_URL`/`POSTGRES_URL`: this -// plugin talks to a different database on (most likely) the same Postgres server, and the platform's -// own connection string must never be reused here by accident — the standalone service's -// `atelier_db/src/env.ts` had a whole enforcement mechanism for exactly this mistake ("a development -// process cannot reach the production database"), which no longer applies once this runs as one -// process under one PM2 entry, but the principle — don't let this plugin's queries land on the -// platform's own tables — still does. -const POSTGRES_URL = process.env.ATELIER_POSTGRES_URL; +// This originally connected to a separate `atelier` database via `ATELIER_POSTGRES_URL`, carried over +// from the standalone service. Reversed on 2026-08-20 by the owner: the tables are `atelier_`-prefixed +// in officer_db, the plugin starts empty, and the standalone service's rows are migrated afterwards +// with someone watching. +// +// The reason is not tidiness. A second database meant a second connection string to set, a second thing +// to back up, and a plugin whose failure modes nobody else's plugin shares. `OFFICERDEV_POSTGRES_URL ?? +// POSTGRES_URL` is the house convention (src/servers/env-prefix.ts), and the standalone repo's whole +// "a development process cannot reach production" enforcement stops applying once this is one process +// under one PM2 entry. +const POSTGRES_URL = process.env.OFFICERDEV_POSTGRES_URL ?? process.env.POSTGRES_URL; if (!POSTGRES_URL) { - throw new Error( - 'ATELIER_POSTGRES_URL environment variable is required — set it in the platform .env to the ' + - '`atelier` Postgres database this plugin should use. See plugins/atelier/PLUGIN.md.', - ); + throw new Error('OFFICERDEV_POSTGRES_URL (or POSTGRES_URL) is required — the platform sets it in .env.'); } /** diff --git a/sidecar/db/generations/schema.ts b/sidecar/db/generations/schema.ts index f6b367e..ca27867 100644 --- a/sidecar/db/generations/schema.ts +++ b/sidecar/db/generations/schema.ts @@ -32,8 +32,7 @@ export type GenerationOperation = (typeof GENERATION_OPERATIONS)[number]; * unauthenticated as ordinary images, so they need to be a file a static route can hand to * `new Response(Bun.file(...))` — not a column something has to decode first. */ -export const generations = pgTable( - 'generations', +export const generations = pgTable('atelier_generations', { /** * Also the filename on disk. One id means one image, so a row can be turned into a served URL @@ -123,11 +122,11 @@ export const generations = pgTable( finishedAt: timestamp('finished_at', { withTimezone: true }), }, (table) => [ - check('generations_status_check', sql`${table.status} in ('queued', 'running', 'succeeded', 'failed')`), - check('generations_operation_check', sql`${table.operation} in ('generate', 'edit')`), + check('atelier_generations_status_check', sql`${table.status} in ('queued', 'running', 'succeeded', 'failed')`), + check('atelier_generations_operation_check', sql`${table.operation} in ('generate', 'edit')`), // The queue reads "what is still to do, oldest first" on every tick and after every restart. - index('generations_status_created_idx').on(table.status, table.createdAt), + index('atelier_generations_status_created_idx').on(table.status, table.createdAt), // Every poll of a job is this lookup. - index('generations_request_idx').on(table.requestId, table.index), + index('atelier_generations_request_idx').on(table.requestId, table.index), ], ); diff --git a/sidecar/db/providers/schema.ts b/sidecar/db/providers/schema.ts index 1e9a840..98c94b5 100644 --- a/sidecar/db/providers/schema.ts +++ b/sidecar/db/providers/schema.ts @@ -18,8 +18,7 @@ import { pgTable, uuid, text, boolean, timestamp, index } from 'drizzle-orm/pg-c * repo is updated, and refusing a new one on release day is a worse failure than passing it through * and letting the vendor answer. */ -export const providers = pgTable( - 'providers', +export const providers = pgTable('atelier_providers', { id: uuid('id').primaryKey().defaultRandom(), @@ -50,6 +49,6 @@ export const providers = pgTable( }, (table) => [ // Resolving a request by model is the hot path: every generation does it once. - index('providers_model_idx').on(table.model, table.enabled), + index('atelier_providers_model_idx').on(table.model, table.enabled), ], );