Skip to content

Known Issues & Limitations

Productify is in early development. This page lists the current known gaps so you can plan around them.

Platform

  • No Kubernetes support yet — supported deployment targets are Docker Compose and HashiCorp Nomad. Kubernetes support is planned for a future release.
  • Single-node Nomad focus — the provided job files and guides target a simple, single-node Nomad setup; multi-region and federated clusters are untested.

Manager

No overview dashboard

The Manager UI opens on the Projects list. An overview dashboard is planned but not yet implemented.

The Manager trusts identity headers unconditionally

The Manager reads X-Token-Subject / X-Token-User-Email / X-Token-User-Name and trusts them without verifying the request came from the proxy — there is no shared proxy secret or equivalent check. Anyone who can reach the Manager directly can impersonate any user. Expose the Manager only behind the proxy and enforce that at the network layer.

Role assignment is not checked against the caller's own role

Resolved in the Wave 3 release train. Authorization checks are fail-closed and now test permissions, not role names; a user may only assign or define roles whose permissions are a subset of their own (no self-escalation). Tenant roles are configurable per project over a fixed permission vocabulary. See the Access Model.

The system-wide user role is not enforced

Every user carries a system role (normal, audit, administrator), but nothing reads it. It grants and restricts nothing — administrator is not an admin. All effective access comes from project, tenant, and application roles.

Pilets are not project-scoped

The pilet endpoints authenticate the caller but never check project membership — they only verify the project exists. Any authenticated PAT holder can list, download, and upload pilets for any project.

The audit log is never written

The AuditLog schema and the auditLogs query exist, but no code creates an AuditLog row, so the table is always empty. The separate ESB audit log is written per endpoint logLevel. The auditLogs query also supports only entityType, action, and description filters, with no operators — unrecognised filter fields are silently ignored rather than rejected.

No backend unregister endpoint

DELETE /api/machine/backend/:id is not implemented, but @productifyfw/node-express's unregister() calls it anyway — the call always fails (it throws rather than silently no-op'ing) and stale registrations persist regardless. There is likewise no dedicated heartbeat route: heartbeating re-POSTs /api/machine/register-backend.

The environment / per-tenant enablement model

Resolved in the Wave 3 release train. The Project → Applications + Environments + Tenants model now ships: applications are defined once at the project level, Environment is a first-class entity (dev | qas | run), and Tenant2Application controls per-tenant enablement and carries the config-cascade leaf. Tenants are customer organizations (tenant.type removed), tenant roles are configurable per project, and triggers fan out per enabled tenant from a single delivery. Upgrading? See the Migration Guide. Invitations and delegated tenant administration remain Wave 4.

CLI

Deployment artifacts are now generated

Resolved in the Wave 3 release train. Deployment artifacts render from specs, not by hand: application Nomad jobs render from a deployment spec on the Manager's Deployment entity (pfy nomad render|deploy), the platform-core stack renders from a local install.yaml (pfy platform render|deploy), per-application vhosts render from Manager data and push live via Caddy's admin API, and pfy deploy diff shows spec-vs-repo-vs-cluster drift. Spec access is scriptable (pfy deployment get|set|snapshot|apply). Rendered files carry a GENERATED header with the spec hash and never contain secrets. See Generated Artifacts.

Browser/SSO login is not yet implemented

pfy login only accepts a manually-issued --server/--token pair; it does not sign in through the identity provider or provision a Personal Access Token automatically. CI continues to use PFY_MANAGER_TOKEN.

Local dev stack (pfy dev) does not exist yet

There is no pfy dev up, pfy dev seed, or pfy dev proxy command. For local environments, use Docker Compose deployment directly.

No structured (JSON/YAML) output

There is no --output/-o flag: every command prints human-readable text only, so -o json | jq … pipelines do not work. IDs must be parsed out of that text — pfy project get --slug <slug> prints an ID: line by default, and list commands print each record's ID only under --verbose. Note that --quiet suppresses output rather than making it machine-readable.

No pfy env command

The environments block in project.yaml is hand-edited. There is no pfy env add|list|remove, and pfy sync does not populate it — sync only refreshes project_id and project_name (it does not update version either).

Integration libraries

Backends cannot authenticate the proxy

@productifyfw/node-express's middleware reads the proxy-injected X-User-* / X-Tenant-* headers unconditionally. There is no proxySharedSecret option and no X-Proxy-Auth verification, so the library cannot distinguish a request forwarded by the proxy from one crafted by any client that can reach the port — identity headers are trivially forgeable on a directly reachable backend. Keep application ports off public networks and reachable only from the proxy.

Pilets are loaded without integrity verification

@productifyfw/core's pilet loader import()s each feed entry's link directly. The feed item's hash field is declared on the type but never checked, and there is no verifyIntegrity option. Since pilets are remote code executed in the browser, only point the loader at feeds and origins you control.

No Go backend library

Backend Integration - Go describes a design, not a shipped library: be-integrations/ contains no Go module, so go get github.com/ProductifyFW/be-integrations/go fails. Use @productifyfw/node-express or call the Manager's REST endpoints directly.

No framework binding packages

@productifyfw/vue, @productifyfw/react, and @productifyfw/dev do not exist — fe-integrations/ publishes only @productifyfw/core. Use core directly. Note that createI18nComposable and the pilet markRaw handling currently live in core, not in a Vue package.

No test-double package

@productifyfw/testing (and the Go managertest double) do not exist. Stub the Manager's HTTP endpoints yourself in tests.

Proxy

Manager-rendered vhosts

Resolved in the Wave 3 release train. Per-application vhost blocks render from Manager data and push to the running proxy via Caddy's admin API (POST :2019/load, graceful reload), with drift detection (pfy proxy diff|sync) and a rendered catch-all that returns 421 for unknown hosts. Onboarding an application or domain is a Manager operation, not a Caddyfile edit. The renderer stamps a minimum proxy version; an incompatible proxy rejects the config loudly. See Generated Artifacts and Application Routing.

Tenant switching

Resolved in the Wave 3 release train. /_tenant re-validates membership and sets an HMAC-signed, per-application cookie; a hosted selector page (GET /_tenant/select) serves multi-tenant users (single-tenant users are auto-selected), and window.__PRODUCTIFY__ exposes available_tenants. The authoritative membership endpoint is the single source of truth for both the selector and per-request enforcement. See Multi-tenancy → Tenant Switching.

Testing

The E2E harness can be shadowed by an orphaned Manager

deployments/e2e/run.sh binds the Manager it starts to :18080 (API) and :18081 (health). Its cleanup runs from trap cleanup EXIT, which does not survive every exit path — an interrupted or hard-killed run can leave the Manager process behind. The preflight used to check :80 (the rendered catch-all) but not :18080/:18081, so a subsequent run happily proceeded against the stale Manager: scenarios passed, and the run looked meaningful when it was testing a binary built from a previous checkout.

Resolved. run.sh now pre-flights both ports and exits 78 naming the port, how to find the process (ss -ltnp 'sport = :18080') and what to do about it, rather than proceeding against a stale binary. The guard earned its place immediately: it caught a Manager left behind by an interrupted run during the Wave 7 work.

Note that PFY_E2E_KEEP=1 leaves the stack up on purpose — tear it down before the next run; that is not this issue.

This is a test-harness hazard, not a product risk: it affects confidence in a run's result, not a deployed system. The product-side record it can mislead you about is Communications — Accepted Risks R5.

Manager

A dedicated tenant is locked out of its own dedicated host

Open. Blocks the Wave 7 dedicated-tenant feature. Found by deployments/e2e/dedicated.test.mjs once the harness seeded real deployment rows.

Excluding dedicated tenants from the shared host (see Dedicated means moved off the shared host) removes the tenant from resolveUserTenantMemberships. But that is the same query the proxy's pinned tenant selector consults, via GET /api/proxy/user/:user_id/tenants?application_id=…, to verify that a caller is a member of the tenant the vhost is pinned to.

A pinned host fails closed by design: an unverifiable membership denies. So once a tenant is excluded from that list, its own dedicated host can no longer confirm that anybody is a member of it, and answers 403 Forbidden: not a member of this tenant to every request — including from users who genuinely belong to that tenant and to no other.

The exclusion is keyed on a pinned vhost demonstrably existing, precisely so that a tenant is never removed from the shared host before their own is ready. The lockout therefore lands exactly in the steady state the feature is for: the moment the dedicated vhost is rendered, the tenant is served nowhere. Both hosts refuse them.

Observed on the E2E stack:

# the pinned vhost, requested by a genuine member of the pinned tenant
GET /comms/api/notifications  ->  403 Forbidden: not a member of this tenant

# the membership list that host consults, for that same user
GET /api/proxy/user/<user>/tenants?application_id=<app>
  -> {"tenants":[{"id":"<tenant-A>","name":"Tenant A",…}]}     # the pinned tenant is absent

A fix has to separate the two questions the endpoint currently conflates — "which tenants may this caller choose on a shared host?" (excludes moved-off tenants) versus "is this caller a member of tenant T?" (must not) — rather than relaxing the pinned host's fail-closed rule, which is load-bearing.

Workaround: none. Keep tenants shared until this is fixed.

deployments/e2e/dedicated.test.mjs scenarios 2, 4 and 6 assert the correct behaviour and are red against the current build; they are the regression tests for this entry.

Proxy

An app vhost's /pilet-feed/ is not reachable at its natural URL

Resolved (Wave 7 security review). Pre-existing (Wave 5-era); surfaced by the Wave 7 dedicated-tenant E2E scenario, and fixed together with a cross-tenant leak that lived on the same two lines.

A rendered application vhost carried a @pilet_feed route that scoped the feed request for the browser by appending the client's own path remainder:

caddyfile
@pilet_feed path /pilet-feed/*
uri strip_prefix /pilet-feed
rewrite /pilet-feed/<app>{uri}            # shared vhost
rewrite /pilet-feed/<app>/<tenant>{uri}   # dedicated vhost

path /pilet-feed/* is a prefix match that requires the trailing slash, so the shortest request that matched was /pilet-feed/. After strip_prefix, {uri} was / — never the empty string. The rewrite therefore always produced a trailing slash (/pilet-feed/<app>/), for which the Manager's router has no route, so it answered 301 to the slash-less form. That redirect leaked the internal, already-scoped path to the client, which re-requested it at the app vhost, where the rewrite prepended the ids a second time.

On a shared vhost the visible result was a 404 reading Application is not enabled for this tenant: the doubled path /pilet-feed/<app>/<app> matched the tenant-scoped feed route with the application id sitting in the tenant slot. The Manager's own vhost (handle /pilet-feed/*, no rewrite) was unaffected, which is why the break survived: every existing test drove the feed there.

The same {uri} was also a cross-tenant leak, which is what made this urgent rather than cosmetic. The route deliberately runs no auth handler — the feed is what an unauthenticated browser bootstraps from — while its reverse_proxy attaches X-Proxy-Auth. So on a shared host, GET /pilet-feed/<victim-tenant-uuid> stripped to /<uuid>, rewrote to /pilet-feed/<app>/<uuid>, and landed on the tenant-scoped route with the proxy authenticating the request on the caller's behalf — an anonymous oracle for another tenant's enabled module set and pinned pilet versions. Before Wave 7 that two-segment path 404'd; adding the tenant-scoped feed route made it live.

The fix is an absolute rewrite — no {uri}, no uri strip_prefix:

caddyfile
@pilet_feed path /pilet-feed/*
rewrite /pilet-feed/<app>                 # shared vhost
rewrite /pilet-feed/<app>/<tenant>        # dedicated vhost

Nothing is lost: both feed routes are fully determined by ids the renderer already knows, and neither takes a sub-path, so the remainder carried no signal — only reach. Query strings still ride along, because Caddy's rewrite preserves them when the target names no query of its own.

/pilet-feed/ on an app vhost is now answered directly with the feed body, and a client-supplied tenant id on a shared host is discarded rather than honoured. deployments/e2e/dedicated.test.mjs scenario 5b is the regression test; it asserts on the response body, because under the vulnerability every one of these requests answered 200 and a status-only assertion would not have caught it.

General

  • Breaking changes may occur between releases while the platform is pre-1.0.
  • Test coverage across components is still being expanded.

If you run into an issue not listed here, please open an issue on GitHub.