one directory per feature: schema.ts and queries.ts together

src/databases/officer_db/src/<feature>/{schema.ts,queries.ts}, replacing the
parallel schema/ and queries/ trees. 24 feature directories, 46 files moved with
git mv so history follows.

The parallel trees had drifted, which is what the restructure is really fixing:

  four features were named differently on each side — app-store/sidecar-installs,
  email/email-accounts, server/server-config

  operations had a schema and NO query file: its task_logs is reached directly
  from src/servers/api/task-logger.ts, bypassing this package's own boundary

  integrations had queries and NO schema, because it spans two features'
  tables — server_integrations and user_integrations

Both lopsided cases survive as directories holding one file, which states the
problem instead of hiding it across two trees.

Nothing outside the package changed how it imports. `officerdb`, `officerdb/types`
and `officerdb/db` resolve exactly as before; index.ts absorbed the path changes.
Added `"./*": "./src/*"` so the new layout is reachable — `officerdb/soulseek/schema`
— which one script needed, because soulseek is a plugin and therefore commented
out of the aggregator.

schema/index.ts became src/schema.ts, keeping the core/plugin split from earlier
tonight. drizzle.config.ts and the package's "./schema" export follow it.

Verified rather than assumed: all 52 files in the package parse, every relative
import resolves against the new layout (checked by walking each specifier to a
real file, since parsing does not check paths), and everything in the tree
importing officerdb still parses. Not typechecked — empty node_modules, frozen
installs.

One rewrite bug worth recording: the rule mapping a query module's sibling import
also matched the './schema' this pass had just written, turning it into
'../schema/queries' in 22 files. Caught by the resolver check, not by parsing —
both spellings parse fine.

Also corrects every path reference the move invalidated: src/databases/CLAUDE.md's
layout diagram, the root CLAUDE.md data section, three docs, and seven sidecar
comments naming queries/<x>.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 01:34:04 +00:00
co-authored by Claude Opus 5
parent 4cebae1c85
commit 68f2c55ecf
63 changed files with 161 additions and 142 deletions
+3 -2
View File
@@ -123,8 +123,9 @@ imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helper
Two stores, and the split matters:
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in
`src/queries/`, types inferred from the schema in `src/types.ts`.
email accounts, queue and pipeline jobs. One directory per feature holding `schema.ts` and
`queries.ts` beside each other; `src/schema.ts` is what `db:push` reads, and it lists the core tables
with the plugin ones commented out. Types inferred from the schema in `src/types.ts`.
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
+1 -1
View File
@@ -327,4 +327,4 @@ service verbs exist (`listApiKeys`, `revokeApiKey`) if that changes.
bearer string into a caller. All four doors call it: `userMiddleware`, `originScopeMiddleware`, the
WebSocket upgrade in `server.tsx`, and the vault socket.
- `src/servers/api/api-keys/router.ts` — the three endpoints.
- `src/databases/officer_db/src/schema/api-keys.ts` — the table, and why it stores what it stores.
- `src/databases/officer_db/src/api-keys/schema.ts` — the table, and why it stores what it stores.
+1 -1
View File
@@ -5,7 +5,7 @@ does and does not protect against.
Authoritative for the crypto design. The code is `src/servers/sidecar/wallet/keys.ts` (sealing,
derivation, unlock sessions), `src/databases/officer_db/src/crypto.ts` (storage encryption) and
`src/databases/officer_db/src/queries/wallet.ts` (where the two meet).
`src/databases/officer_db/src/wallet/queries.ts` (where the two meet).
## The requirement
+3 -3
View File
@@ -135,7 +135,7 @@ and the rename sequence leaves `workspaces` with no zombie.
- [x] **`ws-terminals-{id}: null` on a live dashboard is a 500.** Same file, `:61-66` — the
`ws-layout-*` branch has a `value === null``deleteDashboard` case (`:42`); the terminals
branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column
(`databases/officer_db/src/queries/dashboards.ts:70`) → 23502.
(`databases/officer_db/src/dashboards/queries.ts:70`) → 23502.
**Resolved.** A null on either terminals branch is now a no-op: it means "forget this key", and it
only ever arrives paired with `ws-layout-{id}: null` on a rename, by which point the row is gone.
@@ -202,7 +202,7 @@ these.
> which uuid ids would not.
- [ ] **`dashboards.id` is a global primary key but ids are `slugify(name)`.**
`databases/officer_db/src/schema/dashboards.ts` declares `id: text('id').primaryKey()`. Live:
`databases/officer_db/src/dashboards/schema.ts` declares `id: text('id').primaryKey()`. Live:
`"dashboards_pkey" PRIMARY KEY, btree (id)` plus a redundant
`"uq_dashboards_user_id" UNIQUE, btree (user_id, id)` — evidence per-user ids were intended and
half-built. Ids come from `DashboardPreview.tsx:300` (`slugify(trimmed) || generateSlug()`) and the
@@ -213,7 +213,7 @@ these.
(see `databases/CLAUDE.md` → "Composite keys") — harmless churn, but read the plan.
- [x] **`upsertDashboard`'s UPDATE has no `userId` predicate.**
`databases/officer_db/src/queries/dashboards.ts:73` —
`databases/officer_db/src/dashboards/queries.ts:73` —
`db.update(dashboards).set(set).where(eq(dashboards.id, id))`. The `existing` lookup above it _is_
scoped, so it cannot reach another user's row today, but it is a non-transactional read-then-write.
**It becomes a live cross-user overwrite the moment the PK above is made composite.**
+3 -1
View File
@@ -10,7 +10,9 @@
import type { BrowsedFile } from 'officerdb';
import { eq, asc } from 'drizzle-orm';
import { db, finishSoulseekBrowse } from 'officerdb';
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/schema';
// soulseek is a plugin, so its tables are commented out of officerdb's schema aggregator —
// import them from the feature directly.
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/soulseek/schema';
import { buildTree } from '../src/servers/sidecar/slskd/browse';
const snapshots = await db
+25 -12
View File
@@ -9,18 +9,31 @@ describing a different codebase.
```
src/databases/officer_db/
├── src/
│ ├── db.ts # the connection
│ ├── index.ts # public surface: re-exports queries, schema and drizzle helpers
│ ├── types.ts # every type export (Select / Insert / extended)
── schema/
├── index.ts # re-exports all schema files
└── *.ts # table definitions, grouped by domain
└── package.json # exports "." and "./types"
│ ├── db.ts # the connection
│ ├── index.ts # public surface: re-exports every feature's queries
│ ├── schema.ts # what db:push creates — see below
── types.ts # every type export (Select / Insert / extended)
├── crypto.ts # at-rest encryption, one key per purpose
├── secret-store.ts # the key store itself (SQLite, outside Postgres)
│ └── <feature>/
│ ├── schema.ts # its tables
│ └── queries.ts # everything that reads or writes them
└── package.json # exports ".", "./types", "./db", "./schema", "./secret-store", "./*"
```
Schema files are grouped by domain, not by table: `auth`, `chat-events`, `dashboards`, `email`,
`headscale`, `music`, `operations`, `pipeline-jobs`, `server`, `soulseek`, `user-data`, `vault`,
`wallet`.
**One directory per feature, holding both halves.** Restructured 2026-08-13 from parallel `schema/` and
`queries/` trees, where the two sides had drifted: four features were named differently on each side
(`app-store`/`sidecar-installs`, `email`/`email-accounts`, `server`/`server-config`), `operations` had no
query file at all, and `integrations` had no schema file.
Two directories are still lopsided and say so by their contents: `operations/` has only a schema (its
`task_logs` is reached directly from `src/servers/`, bypassing this package), and `integrations/` has only
queries, because it spans `server` and `user-data`.
**`src/schema.ts` is drizzle-kit's view, not the runtime's.** `drizzle.config.ts` points at it, so a
commented line there removes a table from the DATABASE without removing a line of code — every query
imports its tables from `./schema` inside its own feature directory. That is what lets a fresh install
create only the core tables, with the plugin ones commented out until their plugin is installed.
## Schema changes use `push`, not migrations
@@ -114,8 +127,8 @@ Organise `types.ts` by domain with section comments, mirroring the schema files.
## Queries
Hand-written, one file per domain under `src/queries/`, importing tables from `../schema` and types from
`../types`:
Hand-written, one `queries.ts` per feature directory, importing tables from `./schema` beside it and
types from `../types`:
```ts
import { eq, and } from 'drizzle-orm';
+1 -1
View File
@@ -15,7 +15,7 @@ try {
} catch {}
export default defineConfig({
schema: './src/schema/index.ts',
schema: './src/schema.ts',
out: './migrations',
dialect: 'postgresql',
dbCredentials: {
+3 -2
View File
@@ -7,8 +7,9 @@
".": "./src/index.ts",
"./types": "./src/types.ts",
"./db": "./src/db.ts",
"./schema": "./src/schema/index.ts",
"./secret-store": "./src/secret-store.ts"
"./schema": "./src/schema.ts",
"./secret-store": "./src/secret-store.ts",
"./*": "./src/*"
},
"scripts": {
"generate": "drizzle-kit generate --config=drizzle.config.ts",
@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto';
import { and, asc, eq } from 'drizzle-orm';
import { db } from '../db';
import { agentPanels } from '../schema';
import type { AgentPanelRow } from '../schema/agent-panels';
import { agentPanels } from './schema';
import type { AgentPanelRow } from './schema';
export type AgentPanel = AgentPanelRow;
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, timestamp, uniqueIndex, index } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
/**
* One named agent living in one dashboard panel the address book that lets two chat panels on the
@@ -1,6 +1,7 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import { db } from '../db';
import { apiKeys, users } from '../schema';
import { apiKeys } from './schema';
import { users } from '../auth/schema';
import type { ApiKeySelect } from '../types';
// Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password.
//
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import { db } from '../db';
import { sidecarInstalls } from '../schema';
import { sidecarInstalls } from './schema';
// What the owner has installed from the app store. See ../schema/app-store.ts for why there is no
// userId and why `installed` and `enabled` are separate.
@@ -1,6 +1,6 @@
import { eq, and, lt, sql } from 'drizzle-orm';
import { db } from '../db';
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from '../schema';
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from './schema';
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';
// ── Users ──
@@ -1,8 +1,8 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { roleCapabilities } from '../schema';
import type { UserRole } from '../schema/auth';
import type { CapabilityLevelValue } from '../schema/capabilities';
import { roleCapabilities } from './schema';
import type { UserRole } from '../auth/schema';
import type { CapabilityLevelValue } from './schema';
// Grants, keyed on role. Absence denies — see the table comment.
@@ -1,6 +1,6 @@
import { pgTable, serial, text, timestamp, uniqueIndex, check } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { USER_ROLES } from './auth';
import { USER_ROLES } from '../auth/schema';
// What a ROLE may reach. The subject of a grant is a role, never a user.
//
@@ -1,6 +1,6 @@
import { eq, and, gt, asc, desc, lt } from 'drizzle-orm';
import { db } from '../db';
import { chatSessionEvents } from '../schema';
import { chatSessionEvents } from './schema';
/** Append one outbound event to a session's durable log; returns its global cursor id. */
export async function appendChatEvent(sessionId: string, event: unknown): Promise<number> {
@@ -1,6 +1,6 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { dashboards, screens, dashboardDefaults } from '../schema';
import { dashboards, screens, dashboardDefaults } from './schema';
// ── Full state read ──
@@ -1,7 +1,7 @@
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
/**
* A `LayoutNode` is an object, and the whole write path to these columns is `unknown` the PATCH body is
@@ -2,7 +2,7 @@ import { and, desc, eq, isNull } from 'drizzle-orm';
import argon2 from 'argon2';
import { randomBytes } from 'node:crypto';
import { db } from '../db';
import { davAppPasswords } from '../schema/dav';
import { davAppPasswords } from './schema';
export type DavAppPassword = typeof davAppPasswords.$inferSelect;
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, timestamp, index } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Per-device credentials for CalDAV / CardDAV clients — DAVx5, iOS, macOS, Thunderbird.
//
@@ -1,6 +1,6 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { emailAccounts } from '../schema/email';
import { emailAccounts } from './schema';
import type { EmailAccountInsert, EmailAccountSelect } from '../types';
export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> {
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
export const emailAccounts = pgTable(
'email_accounts',
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { headscaleServers } from '../schema';
import { headscaleServers } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// Headscale server registry access for the officer-headscale sidecar. Callers deal in PLAINTEXT —
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single
// Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and
+40 -40
View File
@@ -18,7 +18,7 @@ export {
blacklistToken,
isTokenBlacklisted,
cleanupExpiredTokens,
} from './queries/auth';
} from './auth/queries';
export {
findLiveApiKeyByHash,
@@ -27,9 +27,9 @@ export {
revokeApiKey,
touchApiKey,
type ApiKeyIdentity,
} from './queries/api-keys';
} from './api-keys/queries';
export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './queries/server-config';
export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './server/queries';
export {
getUserSettings,
@@ -38,7 +38,7 @@ export {
patchUserState,
getDockPaths,
setDockPaths,
} from './queries/user-data';
} from './user-data/queries';
export {
getServerIntegrations,
@@ -51,7 +51,7 @@ export {
upsertUserIntegration,
deleteUserIntegration,
findUserByIntegrationConfig,
} from './queries/integrations';
} from './integrations/queries';
export {
getEmailAccounts,
@@ -61,7 +61,7 @@ export {
updateEmailAccountStatus,
updateEmailAccountSyncMeta,
getAllSyncedAccounts,
} from './queries/email-accounts';
} from './email/queries';
export {
getAllDashboardState,
@@ -73,7 +73,7 @@ export {
deleteScreen,
upsertDefaults,
setDefaultsPanelState,
} from './queries/dashboards';
} from './dashboards/queries';
export {
createPipelineJob,
@@ -86,14 +86,14 @@ export {
deletePipelineJob,
deleteTerminalJobsForUser,
markInterruptedJobs,
} from './queries/pipeline-jobs';
} from './pipeline-jobs/queries';
export {
appendChatEvent,
getChatEventsSince,
getLastChatEventSeq,
pruneChatEventsOlderThan,
} from './queries/chat-events';
} from './chat-events/queries';
export {
listAgentPanels,
@@ -105,8 +105,8 @@ export {
markAgentPanelIntroduced,
deleteAgentPanel,
toAgentPanelView,
} from './queries/agent-panels';
export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './queries/agent-panels';
} from './agent-panels/queries';
export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './agent-panels/queries';
export {
getMusicFavorites,
@@ -122,7 +122,7 @@ export {
deletePlaylist,
addPlaylistItems,
setPlaylistItems,
} from './queries/music';
} from './music/queries';
export type {
FavoriteKind,
GroupedFavorites,
@@ -130,8 +130,8 @@ export type {
NowPlayingInput,
PlaylistSummary,
Playlist,
} from './queries/music';
export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './queries/soulseek';
} from './music/queries';
export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './soulseek/queries';
export {
getSoulseekBrowseSnapshots,
getSoulseekBrowseSnapshot,
@@ -144,7 +144,7 @@ export {
getSoulseekBrowseDirFiles,
getSoulseekBrowseDownload,
deleteSoulseekBrowse,
} from './queries/soulseek';
} from './soulseek/queries';
export type {
BrowseDownloadFile,
BrowsedFile,
@@ -154,7 +154,7 @@ export type {
BrowseLevel,
BrowseTreeSearch,
SoulseekBrowseSnapshot,
} from './queries/soulseek';
} from './soulseek/queries';
export {
listHeadscaleServers,
getActiveHeadscaleCredentials,
@@ -164,8 +164,8 @@ export {
setActiveHeadscaleServer,
deleteHeadscaleServer,
recordHeadscaleProbe,
} from './queries/headscale';
export type { HeadscaleServer, HeadscaleServerCredentials } from './queries/headscale';
} from './headscale/queries';
export type { HeadscaleServer, HeadscaleServerCredentials } from './headscale/queries';
export {
listInvoiceshelfAccounts,
getActiveInvoiceshelfCredentials,
@@ -175,8 +175,8 @@ export {
setActiveInvoiceshelfAccount,
deleteInvoiceshelfAccount,
recordInvoiceshelfProbe,
} from './queries/invoiceshelf';
export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './queries/invoiceshelf';
} from './invoiceshelf/queries';
export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './invoiceshelf/queries';
export {
listJellyfinServers,
getActiveJellyfinCredentials,
@@ -186,8 +186,8 @@ export {
setActiveJellyfinServer,
deleteJellyfinServer,
recordJellyfinProbe,
} from './queries/jellyfin';
export type { JellyfinServer, JellyfinCredentials } from './queries/jellyfin';
} from './jellyfin/queries';
export type { JellyfinServer, JellyfinCredentials } from './jellyfin/queries';
export {
listPhotosAccounts,
getActivePhotosCredentials,
@@ -197,16 +197,16 @@ export {
setActivePhotosAccount,
deletePhotosAccount,
recordPhotosProbe,
} from './queries/photos';
export type { PhotosAccount, PhotosCredentials } from './queries/photos';
} from './photos/queries';
export type { PhotosAccount, PhotosCredentials } from './photos/queries';
export {
listDavAppPasswords,
createDavAppPassword,
revokeDavAppPassword,
deleteDavAppPassword,
verifyDavAppPassword,
} from './queries/dav';
export type { DavAppPassword, DavAppPasswordView } from './queries/dav';
} from './dav/queries';
export type { DavAppPassword, DavAppPasswordView } from './dav/queries';
export {
getServiceConnection,
getServiceCredentials,
@@ -215,17 +215,17 @@ export {
recordServiceProbe,
getServiceInstanceUrl,
getResolvedServiceCredentials,
} from './queries/service-connections';
export type { ServiceName, ServiceConnection, ServiceCredentials } from './queries/service-connections';
} from './service-connections/queries';
export type { ServiceName, ServiceConnection, ServiceCredentials } from './service-connections/queries';
export {
getAllRoleGrants,
getRoleGrants,
setRoleGrant,
revokeRoleGrant,
replaceRoleGrants,
} from './queries/capabilities';
export type { RoleGrant } from './queries/capabilities';
export type { CapabilityLevelValue } from './schema/capabilities';
} from './capabilities/queries';
export type { RoleGrant } from './capabilities/queries';
export type { CapabilityLevelValue } from './capabilities/schema';
export {
getVaultTokens,
setVaultTokens,
@@ -234,8 +234,8 @@ export {
getVaultUnlockKey,
setVaultUnlockKey,
clearVaultUnlockKey,
} from './queries/vault';
export type { VaultTokenSet } from './queries/vault';
} from './vault/queries';
export type { VaultTokenSet } from './vault/queries';
export {
listWallets,
getWallet,
@@ -254,7 +254,7 @@ export {
getWalletChainCache,
saveWalletChainCache,
recordWalletChainError,
} from './queries/wallet';
} from './wallet/queries';
export type {
WalletKind,
WalletSummary,
@@ -262,13 +262,13 @@ export type {
WalletLabel,
CreateWalletParams,
WalletChainCache,
} from './queries/wallet';
export type { WalletChainSnapshot } from './schema/wallet';
} from './wallet/queries';
export type { WalletChainSnapshot } from './wallet/schema';
// Exported as a value, not just a type: the API and the UI need to enumerate the roles, and the
// column definition is the only place that list should exist.
export { USER_ROLES, OWNER_USER_ID } from './schema/auth';
export type { UserRole } from './schema/auth';
export { USER_ROLES, OWNER_USER_ID } from './auth/schema';
export type { UserRole } from './auth/schema';
export { db } from './db';
export * as schema from './schema';
@@ -279,7 +279,7 @@ export {
deletePushDevice,
recordPushFailure,
markPushDeviceSeen,
} from './queries/notify';
} from './notify/queries';
export type { PushDeviceSelect, PushDeviceInsert } from './types';
// App store — what the owner has installed, and whether it should be running.
@@ -294,4 +294,4 @@ export {
setEnabled,
removeInstall,
type SidecarInstall,
} from './queries/sidecar-installs';
} from './app-store/queries';
@@ -1,7 +1,8 @@
import { eq, and, sql } from 'drizzle-orm';
import { db } from '../db';
import { serverIntegrations, userIntegrations } from '../schema';
import { users } from '../schema/auth';
import { serverIntegrations } from '../server/schema';
import { userIntegrations } from '../user-data/schema';
import { users } from '../auth/schema';
import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types';
// ── Server Integrations ──
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { invoiceshelfAccounts } from '../schema/invoiceshelf';
import { invoiceshelfAccounts } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// The InvoiceShelf accounts behind /invoices, for the officer-invoiceshelf sidecar.
//
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { jellyfinServers } from '../schema/jellyfin';
import { jellyfinServers } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// Jellyfin server registry for the officer-jellyfin sidecar. Callers deal in PLAINTEXT — encryption to and
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// The Jellyfin servers behind /jellyfin, for the officer-jellyfin sidecar.
//
@@ -1,6 +1,6 @@
import { eq, and, desc, asc, sql } from 'drizzle-orm';
import { db } from '../db';
import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from '../schema/music';
import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from './schema';
export type FavoriteKind = 'track' | 'album' | 'artist';
export type GroupedFavorites = { tracks: string[]; albums: string[]; artists: string[] };
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets:
// track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id)
@@ -1,6 +1,6 @@
import { eq, and, sql } from 'drizzle-orm';
import { db } from '../db';
import { pushDevices } from '../schema/notify';
import { pushDevices } from './schema';
import type { PushDeviceSelect, PushDeviceInsert } from '../types';
// The push device registry. Only the officer-notify sidecar uses these.
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// Devices that can receive a push, for the officer-notify sidecar.
//
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
export const taskLogs = pgTable('task_logs', {
id: serial('id').primaryKey(),
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { photosConfig } from '../schema/photos';
import { photosConfig } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// The Immich accounts behind /photos, for the officer-photos sidecar.
//
@@ -2,7 +2,7 @@ import { eq, and, inArray, asc, desc } from 'drizzle-orm';
const TERMINAL_STATUSES = ['completed', 'failed', 'stopped', 'interrupted'] as const;
import { db } from '../db';
import { pipelineJobs } from '../schema/pipeline-jobs';
import { pipelineJobs } from './schema';
import type { PipelineJobInsert } from '../types';
export async function createPipelineJob(data: PipelineJobInsert) {
@@ -1,5 +1,5 @@
import { pgTable, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
export const pipelineJobs = pgTable(
'pipeline_jobs',
@@ -21,35 +21,35 @@
// ── Core ─────────────────────────────────────────────────────────────────────────────────────────
export * from './auth'; // users, passkeys, passkey_challenges, token_blacklist
export * from './capabilities'; // role_capabilities — what each ROLE may reach
export * from './api-keys'; // api_keys
export * from './user-data'; // user_settings, user_state, user_integrations, dock_configs
export * from './dashboards'; // dashboards, screens, dashboard_defaults
export * from './server'; // server_config (SMTP lives here), server_integrations
export * from './operations'; // task_logs, queue_jobs, terminal_containers
export * from './pipeline-jobs'; // pipeline_jobs
export * from './chat-events'; // chat_session_events
export * from './agent-panels'; // agent_panels
export * from './auth/schema'; // users, passkeys, passkey_challenges, token_blacklist
export * from './capabilities/schema'; // role_capabilities — what each ROLE may reach
export * from './api-keys/schema'; // api_keys
export * from './user-data/schema'; // user_settings, user_state, user_integrations, dock_configs
export * from './dashboards/schema'; // dashboards, screens, dashboard_defaults
export * from './server/schema'; // server_config (SMTP lives here), server_integrations
export * from './operations/schema'; // task_logs, queue_jobs, terminal_containers
export * from './pipeline-jobs/schema'; // pipeline_jobs
export * from './chat-events/schema'; // chat_session_events
export * from './agent-panels/schema'; // agent_panels
// Core because the tailnet is the perimeter — a security model resting on it cannot treat administering
// it as an optional extra. The secret store bootstraps a `headscale` key on this basis.
export * from './headscale'; // headscale_servers
export * from './headscale/schema'; // headscale_servers
// The app store itself, and the credentials it stores for what it installs. `app-store/effects.ts`
// reads service_connections, so this is core however few plugins are installed.
export * from './app-store'; // sidecar_installs
export * from './service-connections'; // service_connections
export * from './app-store/schema'; // sidecar_installs
export * from './service-connections/schema'; // service_connections
// ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
// export * from './email'; // email_accounts officer-email
// export * from './music'; // music_favorites, _playlists, _playlist_items, _now_playing
// export * from './notify'; // push_devices officer-notify
// export * from './dav'; // dav_app_passwords officer-caldav
// export * from './photos'; // photos_config officer-photos
// export * from './jellyfin'; // jellyfin_servers officer-jellyfin
// export * from './invoiceshelf'; // invoiceshelf_accounts officer-invoiceshelf
// export * from './soulseek'; // soulseek_favorites, _browse_snapshots, _browse_dirs
// export * from './vault'; // vault_tokens, vault_unlock_keys officer-vault
// export * from './wallet'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache
// export * from './email/schema'; // email_accounts officer-email
// export * from './music/schema'; // music_favorites, _playlists, _playlist_items, _now_playing
// export * from './notify/schema'; // push_devices officer-notify
// export * from './dav/schema'; // dav_app_passwords officer-caldav
// export * from './photos/schema'; // photos_config officer-photos
// export * from './jellyfin/schema'; // jellyfin_servers officer-jellyfin
// export * from './invoiceshelf/schema'; // invoiceshelf_accounts officer-invoiceshelf
// export * from './soulseek/schema'; // soulseek_favorites, _browse_snapshots, _browse_dirs
// export * from './vault/schema'; // vault_tokens, vault_unlock_keys officer-vault
// export * from './wallet/schema'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import { db } from '../db';
import { serverConfig } from '../schema';
import { serverConfig } from './schema';
const SETTINGS_KEY = 'server-settings';
@@ -1,7 +1,7 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { serviceConnections } from '../schema';
import { getOwnerUser } from './auth';
import { serviceConnections } from './schema';
import { getOwnerUser } from '../auth/queries';
import { encryptSecret, decryptSecret } from '../crypto';
// Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT —
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Where a self-hosted service lives, and what it takes to talk to it — for services the owner has exactly
// ONE of. Transmission and slskd today.
@@ -1,6 +1,6 @@
import { eq, and, asc, isNull, inArray, sql } from 'drizzle-orm';
import { db } from '../db';
import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from '../schema/soulseek';
import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from './schema';
/** A user's favourited Soulseek peers, alphabetical (the order the UI lists them in). */
export async function getSoulseekFavorites(userId: number): Promise<string[]> {
@@ -10,7 +10,7 @@ import {
foreignKey,
uniqueIndex,
} from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Soulseek state that Officer owns because slskd has none. slskd exposes no favourites/buddy-list API
// (verified against 0.26.0's UsersController: only endpoint/browse/directory/info/status), so the peers
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import { db } from '../db';
import { dockConfigs, userSettings, userState } from '../schema';
import { dockConfigs, userSettings, userState } from './schema';
// ── User Settings ──
@@ -1,7 +1,7 @@
import { pgTable, serial, integer, text, timestamp, jsonb, check, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
import { sql } from 'drizzle-orm';
import { serverIntegrations } from './server';
import { serverIntegrations } from '../server/schema';
export const userSettings = pgTable('user_settings', {
userId: integer('user_id')
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import { db } from '../db';
import { vaultTokens, vaultUnlockKeys } from '../schema/vault';
import { vaultTokens, vaultUnlockKeys } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// Vault store access. Callers deal in PLAINTEXT — encryption to/from at-rest ciphertext happens here, so
@@ -1,5 +1,5 @@
import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { users } from '../auth/schema';
// Officer Vault server-side state (see VAULT_AUTH_SPEC.md). The device never holds a Vaultwarden token;
// the platform brokers it, stores it here tied to the owner account, and injects it on proxied /api/vault
@@ -1,7 +1,7 @@
import type { WalletChainSnapshot } from '../schema/wallet';
import type { WalletChainSnapshot } from './schema';
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from '../schema/wallet';
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';
// Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet'
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
import { users } from '../auth/schema';
// Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a
// self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning /
+1 -1
View File
@@ -5,7 +5,7 @@
// auth+forward proxy and holds NO InvoiceShelf credentials.
//
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `invoiceshelf_accounts` (see
// databases/officer_db/src/queries/invoiceshelf.ts). It is deliberately no longer read from the environment:
// databases/officer_db/src/invoiceshelf/queries.ts). It is deliberately no longer read from the environment:
// Bun auto-loads `.env` into every process started in the platform directory, so an `INVOICESHELF_TOKEN`
// there was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no
// code to use it and the largest attack surface in the system. Nothing in this file reads process.env.
+1 -1
View File
@@ -5,7 +5,7 @@
// thin auth+forward proxy and holds NO Jellyfin credentials.
//
// The server is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `jellyfin_servers` (see
// databases/officer_db/src/queries/jellyfin.ts). Nothing in this file reads process.env — Bun auto-loads
// databases/officer_db/src/jellyfin/queries.ts). Nothing in this file reads process.env — Bun auto-loads
// `.env` into every process started in the platform directory, so a token there would also be sitting in
// `officer`'s own environment: a credential held by the one process that has no code to use it.
//
+1 -1
View File
@@ -4,7 +4,7 @@
// auth+forward proxy and holds NO Immich credentials.
//
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `photos_config` (see
// databases/officer_db/src/queries/photos.ts). It is deliberately no longer read from the environment:
// databases/officer_db/src/photos/queries.ts). It is deliberately no longer read from the environment:
// Bun auto-loads `.env` into every process started in the platform directory, so an `IMMICH_API_KEY` there
// was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no code
// to use it and the largest attack surface in the system. Nothing in this file reads process.env.
+1 -1
View File
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
// auth+forward proxy and holds NO slskd credentials.
//
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see
// databases/officer_db/src/queries/service-connections.ts). It is deliberately no longer read from the
// databases/officer_db/src/service-connections/queries.ts). It is deliberately no longer read from the
// environment: Bun auto-loads `.env` into every process started in the platform directory, so an
// `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole
// Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env.
+1 -1
View File
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
// a thin auth+forward proxy and holds NO Transmission credentials.
//
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see
// databases/officer_db/src/queries/service-connections.ts), no longer read from the environment: Bun
// databases/officer_db/src/service-connections/queries.ts), no longer read from the environment: Bun
// auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also
// sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the
// server. Nothing in this file reads process.env.
+1 -1
View File
@@ -4,7 +4,7 @@ import { getKey } from 'officerdb/secret-store';
// The ONLY reader of WALLET_* env in the tree. Everything else — node URLs, macaroons, runes, LNDHub
// credentials, NWC URIs — is per-wallet configuration the owner enters at runtime and lives encrypted in
// Postgres (databases/officer_db/src/schema/wallet.ts), not here. Env holds only what is genuinely
// Postgres (databases/officer_db/src/wallet/schema.ts), not here. Env holds only what is genuinely
// deployment-wide: which chain we're on.
//
// WHERE CHAIN DATA COMES FROM IS NOT ENV. It used to be WALLET_ESPLORA_URL, which meant the one setting
@@ -1,7 +1,7 @@
// Shared types/constants for the /wallet workspace panels.
//
// The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in
// src/databases/officer_db/src/queries/wallet.ts. They are restated here rather than imported because the
// src/databases/officer_db/src/wallet/queries.ts. They are restated here rather than imported because the
// officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser
// bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never
// reach the client. Keep this file in step with those two by hand; the field names are identical on