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 .env files, 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 /data from 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-gpt or 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:

  1. validate OAuth issuer, audience/resource, expiry, client, scopes, and revocation for every MCP request;
  2. 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;
  3. resolve project ownership, direct shares, group shares, and view versus edit permission server-side;
  4. derive the filesystem owner ID and slug only from the authorized project row;
  5. require explicit scopes for reads, writes, destructive actions, exports, administration, and optional provider operations; tool annotations are descriptive and are never authorization;
  6. 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;
  7. apply bounded input/output schemas, pagination, quotas, idempotency keys, per-workspace mutation serialization, and audit request IDs; and
  8. 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-projects workspace 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 @latest during 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 .dockerignore and 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.md and loadtest/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.sh and 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.


Pm Gpt Hosted Data Boundary local
Report an issue