pm-gpt Hosted Data Boundary
Status: mandatory private deployment policy
Scope: hosted pm-gpt remote MCP and hosted pm-web data plane
Last reviewed: 2026-07-11
The hosted services pm-web.unbrained.dev, pm-gpt.unbrained.dev, and
pm-mcp.unbrained.dev are publicly available, internet-facing user services.
Their hosted-service source repositories are private/closed source, and their
operational configuration, credentials, and user/tenant data remain private.
"Private hosted" in this document refers to that repository/data boundary,
never to service availability.
This document defines where public pm software ends and private closed-source hosted-service code, configuration, credentials,
and user data begin. It is a release gate, not a description of local pm CLI or local MCP behavior. The official
local pm CLI/MCP continues to operate on a user's local workspace and is outside the hosted pm-gpt data plane.
Repository ownership boundary
| Location | Classification | Allowed contents | Must never contain |
|---|---|---|---|
/home/steve/container/pm-web |
Public checkout and public @unbrained/pm-web package |
Generic reusable server/UI source, public schemas, tests, generic self-host examples, neutral legal-page templates | Hosted .env files, service credentials, OAuth clients, database dumps, user workspaces, backups, private pm-gpt source, hosted operator-only configuration |
/home/steve/container/pm-cli |
Private production companion/deployment repository | Private Compose and operations configuration, public website source, deployment documentation, non-secret environment-variable references, pm tracking | Secret values, database/volume contents, dumps, exports, backups, private keys, OAuth client secrets |
/home/steve/container/pm-gpt |
Private hosted remote-MCP service repository | MCP server, OAuth/resource-server integration, tenant policy, private deployment source, migrations owned by pm-gpt, tests and private operational docs | User data snapshots, production secrets committed to Git, public-package registration that exposes the private source repository |
| Docker volumes and databases | Private runtime data | Production state only, protected by runtime credentials and least privilege | Git remotes, package publication, source-control synchronization |
The production Compose project is the private checkout at /home/steve/container/pm-cli. The similarly named
/home/steve/container/pm-cli-companion directory is a separate stale clone and is not the deployment source of
truth. Production must not be built by copying private files into the public pm-web checkout.
Public documentation may name pm-gpt, link to its hosted endpoint, explain user-facing behavior, and record that
the backend is private. It must not publish the private repository URL as an install source, add pm-gpt to the pm
extension/package registry, embed its server source, or imply that it is an installable open-source pm package.
Allowed and prohibited artifacts
Allowed in the private companion repository:
- Compose declarations that reference secret environment-variable names without values;
- operational documentation, architecture decisions, threat models, and pm tracker records;
- public website copy that links to the hosted service without registering it as a pm package;
- read-only validation scripts such as
scripts/check-pm-gpt-data-boundary.sh; and - generic examples containing unmistakable placeholders.
Prohibited in every Git repository:
- production
.envfiles, access/refresh tokens, PATs, passwords, signing or encryption keys; - PostgreSQL dumps, Neo4j stores, SQLite runtime databases, workspace archives, DSAR exports, or backups;
- copied contents of
/app/projects,/var/lib/postgresql/data, or/datafrom Neo4j; - production OAuth client registrations or reviewer credentials; and
- access logs, request/response bodies, tool results, or debug fixtures derived from real users.
Prohibited specifically in the public pm-web repository/package and public package catalogs:
- private pm-gpt backend/plugin source or an install command for it;
github.com/unbraind/pm-gptor another private source locator in extension/package metadata;- hosted legal/operator configuration where a neutral runtime-injected template is sufficient;
- production hostnames for private database, queue, graph, or internal API services; and
- runtime-generated project, search-index, graph, identity, or authorization data.
The public pm-web npm package uses an explicit package-file allowlist. That is helpful but not sufficient: the
public checkout also requires a deny-by-default .dockerignore, hosted artifacts must never be staged there, and
production builds must consume a reviewed immutable release rather than the mutable checkout.
Hosted three-store data model
The existing hosted service is not database-only. A remote MCP implementation that connects only to PostgreSQL cannot provide complete pm behavior.
| Store | Current responsibility | Runtime location |
|---|---|---|
| PostgreSQL | Accounts, password hashes, project index, group membership, sharing ACLs, admin audit metadata, optional encrypted GitHub PATs, project-to-issue links | pm-cli_postgres-data; currently the same database also contains telemetry tables |
| pm workspace filesystem | Authoritative pm settings, item documents, history, comments, notes, plans, links, search indexes, and extension state | pm-cli_pm-web-projects, mounted at /app/projects; paths are keyed by owner user ID and project slug |
| Neo4j | Derived/project-scoped graph nodes and relationships | pm-cli_neo4j-data and pm-cli_neo4j-logs |
PostgreSQL project rows are indexes and authorization anchors; they are not a replacement for workspace files. Neo4j is derived state, but it can still contain private item metadata and must receive the same tenant, deletion, backup, and logging protections.
Successful pm-gpt workspace writes publish content-free invalidations through PostgreSQL LISTEN/NOTIFY on
pm_workspace_events. Public pm-web processes subscribe and forward them only to authenticated local SSE sessions
for that project. The event payload allowlist contains bounded identifiers/change labels only; private item bodies,
emails, credentials, tokens, and paths are prohibited. Notifications are an ephemeral refresh signal, never an
authorization decision, audit log, durable queue, or source of truth.
The preferred integration is one private tenant-aware workspace/data API used by both hosted pm-web and pm-gpt. It must resolve identity and ACLs before opening a workspace, serialize mutations with pm's lock semantics, call the supported pm CLI/SDK boundary, and update derived graph state. pm-gpt should not duplicate SQL authorization queries or receive unrestricted database credentials. If a transitional direct-volume worker is unavoidable, it must run behind the same authorization service, use an allowlisted command surface, and never expose a general remote shell.
Identity, ACL, and API contract
ChatGPT/Codex connects to the hosted remote MCP at pm-gpt; it does not authenticate by possessing a GitHub account. GitHub and other providers are optional integrations.
The hosted contract must:
- validate OAuth issuer, audience/resource, expiry, client, scopes, and revocation for every MCP request;
- map the authenticated external principal to one durable local principal without trusting a model-supplied email, user ID, workspace path, project ID, or item ID;
- resolve project ownership, direct shares, group shares, and
viewversuseditpermission server-side; - derive the filesystem owner ID and slug only from the authorized project row;
- require explicit scopes for reads, writes, destructive actions, exports, administration, and optional provider operations; tool annotations are descriptive and are never authorization;
- use short-lived access tokens and a revocable grant model; never share pm-web's symmetric JWT signing secret with pm-gpt or mint arbitrary pm-web user tokens;
- apply bounded input/output schemas, pagination, quotas, idempotency keys, per-workspace mutation serialization, and audit request IDs; and
- return only the minimum authorized data needed by the selected tool. Logs must not contain item bodies, tool results, credentials, authorization headers, OAuth codes, or tokens.
New users must be able to create and use hosted pm projects without configuring GitHub. GitHub-specific tools may be unavailable until the user explicitly authorizes a dedicated least-privilege integration, but project, item, planning, context, history, annotation, validation, import/export, and other safe pm features must not depend on GitHub identity or storage.
Database and network isolation
Before production pm-gpt access, hosted pm application data must be separated from telemetry privileges. Use a dedicated database or schema with at least these roles:
- a migration owner used only during an explicit deployment migration;
- a least-privilege pm-web runtime role;
- a least-privilege workspace/data-API role; and
- no direct SQL role for pm-gpt when the workspace API is sufficient.
Runtime application roles must not own schemas, run arbitrary DDL, truncate tables, or read telemetry. Database and Neo4j networks must be private backend networks; Caddy joins only service frontend networks. pm-gpt access to the workspace volume, database, graph, or provider credentials must be explicit and minimal.
The private deployment defines internal network pm-cli_pm-data for only Postgres, pm-web, and Neo4j. pm-web runs
as UID/GID 10001 after a networkless one-shot initializer corrects existing workspace ownership and removes
world permissions. The long-running process also inherits umask 0027 for future workspace state. This
configuration remains a pending deployment change until its secrets, public pm-web build,
backup readiness, and maintenance procedure are approved.
The optional pm_gpt_runtime SQL role is provisioned only through the reviewed operator migration in
ops/sql/pm-gpt-runtime-role.sql. It has explicit grants on pm identity/project/ACL tables and no telemetry-table,
delete, sequence, persistent-schema, superuser, database-owner, or role-admin grants. The default PostgreSQL
PUBLIC temporary-table privilege cannot be denied for only one role; revoking it database-wide requires a
separate impact-reviewed migration.
Signing keys, provider-token encryption keys, database passwords, Neo4j passwords, session secrets, and OAuth client secrets are separate values with independent rotation plans. No public/default production password is allowed. Containers run as dedicated non-root users and volumes are owned by their service UID/GID.
The durable pm stores were migrated from Docker named volumes to explicit host bind mounts on 2026-07-11 under
/home/steve/pm-hosted-data/ (outside every Git checkout and public build context):
| Store | Host bind path | Owner/mode | Mounted by |
|---|---|---|---|
| PostgreSQL (pm + telemetry) | /home/steve/pm-hosted-data/postgres |
70:70 700 |
pm-telemetry-postgres |
| pm workspace filesystem | /home/steve/pm-hosted-data/pm-web-projects |
10001:10001 750 |
hosted pm-web and pm-gpt (same path) |
| Neo4j data | /home/steve/pm-hosted-data/neo4j-data |
7474:7474 777 |
pm-graph-neo4j |
| Neo4j logs | /home/steve/pm-hosted-data/neo4j-logs |
7474:7474 777 |
pm-graph-neo4j |
Databases (Postgres, Neo4j) are bind-mounted on local disk only — never NFS — because NFS breaks database
locking and risks corruption. Large cold data (backups) may live on the NAS; live databases may not. The
migration was performed after freeing root-filesystem headroom (relocating ~34 GiB of backups to the NAS and
pruning Docker build/dangling artifacts), with a fresh verified Postgres dump, stopped-store consistent copies,
byte-and-file verification, and preserved ownership/modes. The pre-migration Docker named volumes
(pm-cli_postgres-data, pm-cli_pm-web-projects, pm-cli_neo4j-data, pm-cli_neo4j-logs) are retained
unchanged as an instant rollback (revert the compose mount lines and up -d) and may be pruned once the bind-mount
deployment is confirmed stable. Runtime bind paths must remain outside every Git checkout and public build context.
Backup, restore, export, and deletion obligations
A successful PostgreSQL dump is not a complete hosted pm backup. The private backup set must include, at one consistent logical checkpoint:
- pm application PostgreSQL schema/data;
pm-cli_pm-web-projectsworkspace contents, including history and project configuration;- Neo4j data or a documented, tested rebuild from the authoritative workspace data;
- pm-gpt identity/grant, audit, idempotency, and job records; and
- attachment/object data introduced by pm-gpt.
Backups containing user data or .env material must be encrypted, access-controlled, integrity-checked, retained
for a documented period, and restored in a non-production drill. Backup manifests may be tracked only when they
contain no secret, user, or private runtime values.
The private backup script now includes the workspace and Neo4j volumes, uses restrictive process umask, and omits
production .env from its non-secret config archive. These are necessary improvements, but live tar snapshots and
unencrypted user-data archives do not yet satisfy the complete P0 consistency/encryption gate.
DSAR export and user/workspace export must cover all three stores plus pm-gpt records and attachments. The export must be portable, documented, bounded to the authenticated subject, and contain no other tenant's data.
Deletion is complete only when the database rows, workspace directories, graph nodes, provider grants/tokens, pm-gpt identity links, queued jobs, attachments, caches, and applicable logs are deleted or placed into a documented backup-expiry process. An admin database cascade alone is insufficient. Deletion must be idempotent, audited without retaining deleted content, and verified across every store.
Deployment and build pinning
Production must consume an immutable, reviewed public pm-web artifact:
- pin the pm-web version and preferably an OCI image digest or verified source commit;
- pin the pm CLI/SDK version inside the image; do not install
@latestduring a production build; - build from a clean release artifact, not whichever branch or uncommitted state happens to exist in
/home/steve/container/pm-web; - keep hosted Compose overlays, secret references, legal content, and runtime policy in private deployment inputs;
- record artifact versions/digests in the private deployment evidence; and
- verify the live pm-web, pm CLI, schema, and pm-gpt versions after deployment.
No deployment step may copy a hosted .env, volume, dump, private source tree, or generated user artifact into a
public build context, npm tarball, release asset, package registry, or public CI artifact.
P0 gates before pm-gpt can access hosted user data
- Approve the three-store authoritative-data and workspace API design.
- Prove OAuth discovery/resource metadata, principal mapping, scopes, revocation, and account recovery.
- Remove shared telemetry/application superuser access and deploy least-privilege database roles.
- Isolate database/Neo4j/backend networks from Caddy and unrelated observability services.
- Set independent strong JWT, encryption, OAuth, database, and Neo4j secrets; migrate legacy plaintext tokens.
- Pin immutable pm-web and pm CLI/SDK artifacts and make builds reproducible.
- Add a deny-by-default public pm-web
.dockerignoreand verify the release/build context contains no hosted artifacts. - Replace hosted legal/operator configuration in the public pm-web package with neutral templates plus a private runtime-injected hosted configuration.
- Run containers as non-root and verify volume permissions.
- Implement complete encrypted backup/restore coverage for PostgreSQL, workspaces, graph, and pm-gpt state.
- Implement and test complete per-user export and deletion across every store and backup-expiry path.
- Add two-tenant negative tests for every MCP read, write, export, graph, sharing, admin, and provider path.
- Prove the intended per-project concurrent-user target with authenticated load/soak tests, defined latency and
error budgets, file-descriptor/heap/database limits, reconnect storms, lock contention, and zero tenant crossover.
(Substantial progress 2026-07-12: authenticated 1000- and 2000-client single-project runs passed with 100%
connect/delivery, zero soak drops, measured lock-serialized write contention, and an unbuffered through-edge
SSE check — see pm-gpt
docs/REALTIME_COLLABORATION_AND_CAPACITY.mdandloadtest/results-*.json. Still open: reconnect storms, token expiry under load, server-restart recovery at scale, the >2000 ceiling, and the async pm-runner fix for event-loop stalls behind write bursts.) - Migrate Docker named volumes to approved host bind paths after disk-headroom, consistent-backup, permissions,
maintenance, integrity, and rollback gates pass. (Done 2026-07-11:
/home/steve/pm-hosted-data/; named volumes retained as rollback; databases on local disk, backups relocated to NAS.) - Ensure access/application logs redact OAuth codes, tokens, credentials, private bodies, and tool results with a documented retention period.
- Pass
scripts/check-pm-gpt-data-boundary.shand repository secret scanning before every release/deployment. - Confirm public pm-web/package catalogs contain no private pm-gpt source registration or hosted artifacts.
Until every applicable P0 gate is satisfied, pm-gpt may be developed and tested with synthetic data only. It must not be granted production database, workspace-volume, Neo4j, provider-token, or user-data access.
Local boundary check
Run from the private companion repository:
scripts/check-pm-gpt-data-boundary.sh
The check reads Git's tracked-file index and tracked contents only. It does not open .env, runtime volumes,
databases, backups, or ignored files. Failures print the affected tracked path and rule, never matching content or
secret values. Passing this check is necessary but does not replace a secret scanner, tenant-isolation tests, or
the P0 review above.