Deployment ownership and application access

One logical executor deployment belongs to one owner organization, currently Soter Labs. Several web services/frontends and a worker can share that deployment. Separate environments require separate backing resources. BA Labs, Soter Labs and other Auth0 organizations remain login/affiliation contexts; they are not the owner of every process their members create. No accounts need to move organizations.

Required policy, not a feature flag

Main, standalone backend and worker use createDeploymentApplicationHost. Configure exactly one of PROCESS_ACCESS_CONFIG (private JSON file path) or PROCESS_ACCESS_CONFIG_JSON (inline JSON). Missing, invalid or conflicting configuration fails closed. There is no legacy authorization fallback. PROCESS_BACKEND_CONFIG.applicationAccess is retired and rejected. Backend template/assets/effect provisioning is still separate from authorization.

A synthetic policy:

{
  "deploymentId": "example-production",
  "ownerOrganizationId": "example-owner",
  "defaultApplicationId": "operator-workflows",
  "applications": [
    {
      "applicationId": "spell-review",
      "templateKeys": ["spell-request"],
      "documentKeys": ["spell-strategic-overview"],
      "access": {
        "roles": [{ "key": "reviewer", "permissions": ["templates:read", "processes:read", "processes:write", "spell:review-risk"] }],
        "members": [{ "email": "reviewer@example.test", "roles": ["reviewer"] }]
      }
    },
    {
      "applicationId": "operator-workflows",
      "templateKeys": [],
      "access": { "roles": [], "members": [] }
    }
  ],
  "operators": [{ "email": "operator@example.test", "permissions": ["templates:read", "database:read"] }]
}

Apps declare roles and email-to-role assignments. The executor authenticates and resolves verified identities and implements those grants. templateKeys and optional documentKeys are operator-owned resource bindings, not fields an app user can use to claim someone else's resources. The optional defaultApplicationId assigns newly authored, otherwise-unassigned template keys to an explicit app; it never restores old authorization. Without it every key needs an explicit assignment.

Email after trimming is the membership identifier, using exact case consistently with provider lookup. Case-only duplicate declarations are rejected. There is no email-change, account-linking or mailbox inheritance subsystem: change membership explicitly. Role addresses may intentionally survive staff changes. Provider IDs remain internal authentication/audit details. Unverified or blocked identities are not grandfathered in merely because an address appears in the policy.

App grants are confined to their app. They cannot grant database:*, user:impersonate or templates:write. Publication is deploy-only: templates are code-owned and published at startup when their compiled digest changes; no user, operator or app role can author runnable definitions over HTTP. templates:write is retired — a policy that still lists it as an operator permission loads and the grant is ignored. App template reads remain scoped. Membership and role edits are reread on requests; changing deployment/app identities or resource ownership requires a restart and reviewed migration, not an in-place reassignment of live data.

Enforcement and presentation

Canonical processes persist {deploymentId, ownerOrganizationId, applicationId} in ownership. Children inherit the scope. Scoped repositories filter reads/listing and reject foreign writes, audit and deletes. Template ownership must agree with process ownership. Canonical stores use an owner namespace; document, file and canonical backing resources also acquire immutable one-owner claims, preventing accidental reuse by another deployment. These are trusted platform adapters, not app-author supplied authorization implementations.

Every process command derives permissions for the target app afresh. A combined permission summary may be used for coarse HTTP admission, never as final target authorization. /api/me returns platform permissions plus an explicit applicationPermissions map by default. Query processId or templateKey for target-scoped permissions. Runners use that scope for field/step affordances; list rows carry effectivePermissions. Browser navigation may use a union only to show available destinations. The private-notes probe uses process-scoped identity, and document export checks the asset's declared app membership. Neither can borrow a permission from another app.

Trusted application scope for lists

GET /api/process walks every application the caller is a member of. An app frontend's server-side /api proxy narrows that walk: it sets X-Process-Platform-Application from its own deployment configuration (PROCESS_APPLICATION_ID, validated like PROCESS_AUTH_MOUNT_ID) and always replaces or removes any browser-supplied value. The backend then runs only that application's branch; an unknown id is a 404 and a malformed one a 400. Without the header (Main, direct API use) behaviour is unchanged. This is a scoping default, not a security boundary: membership and permissions still decide every row, so a non-member of the scoped app gets an empty list, never another app's rows. It is consistent with "scope is trusted deployment configuration, not a request header": the value comes from the frontend's deployment configuration, and a caller who sets it directly can only narrow their own view. Records and exports stay cross-application (their pages offer a template picker).

With templates=shared the list sends each distinct summary template once (templates, keyed by template key + updatedAt) and rows carry templateRef. The legacy embedded array remains the default for one release; every in-repo consumer requests the shared shape and expands it with expandProcessList from @processos/contracts/process, which accepts both.

Member discovery resolves declared emails across affiliations. Process sharing can narrow/widen access within declared app membership; it cannot bypass app membership or permit anonymous access. The login organization header must still agree with the verified session. Body emails, unsigned token decoding and frontend identity/mount names never manufacture authority.

Identity provider and onboarding

Existing Auth0 organizations/accounts are retained for login. Provider role claims no longer grant application or operator authority. Operator impersonation resolves the target's identity and declared app permissions; the real actor remains separately identified. App grants cannot enable impersonation.

Identity resolution is performance-critical: production access tokens carry no email claims, so each request needs the provider profile. One HTTP request resolves its principal once (guards, actor and every app branch share it); the /api middleware only verifies the token and never calls the provider. The Management API token is reused until 60 s before expires_in, with concurrent callers sharing one fetch, and is dropped if the API rejects it. verifiedIdentity keeps a bounded per-user cache (PROCESS_IDENTITY_CACHE_TTL_MS, default 30000, 0 disables; PROCESS_IDENTITY_CACHE_MAX_ENTRIES, default 1000) with single-flight lookups. The TTL is the revocation lag: a blocked user, a lost verification or an email change can keep resolving to the cached identity for up to that long on each web process. Failures and unverified results are never cached. Membership edits are unaffected: the policy is reread on every request.

Starting a host never creates users, sends invitations or changes Auth0. The trusted single-app management helper can assess declared members and explicitly invite them using operator-configured onboarding organization/client settings. Invitations assign no global provider roles and do not return bearer invitation URLs. Unknown external outcomes stop for reconciliation, not automatic retry. A membership editing UI and general app-management API remain follow-ups. Actual Auth0 connections, Management API scopes, invitations and login on each deployed mount require live acceptance.

OAuth login now binds signed state to a per-flow, HttpOnly, SameSite=Lax browser cookie. HTTPS cookies are Secure and host-only; only loopback development may use HTTP. Missing/mismatched browser binding is rejected before code exchange. Silent retry retains the flow binding; terminal callbacks clear it.

Migration and activation

This is the only model in the candidate release, not a claim that production has been changed. All web hosts and workers must cut over together; old writers cannot remain an alternate access path. The CLI requires --ownership /private/ownership-by-template.json, mapping every template key to its explicit scope. No creator/email/affiliation inference is used. The source mapping/hash remains in the plan; missing/conflicting assignments block import. Use the owner-aware stores, not legacy unnamespaced readers, for import/readback. Unowned records are never adopted on first access.

Review actual membership and resource mappings before activation. A preservation-oriented draft may contain surprisingly broad historic control-plane grants; carrying them forward is not approval of them. Duplicate-email unions and unverified accounts need explicit review. Keep policy/export files private and outside source control. Prefer a private file for large policies rather than one oversized environment string.

Uploads are byte-bounded while streaming, before multipart parsing and Blob buffering. New files get server-stamped activity/uploader metadata. Deletion checks activity, process access, current field permissions/editability and cross-activity references. Legacy files without reliable ownership are not guessed: deletion fails closed until reviewed metadata migration. Preserve their bytes and sidecars.

See cutover for backup, import, deployment holds, acceptance and rollback gates. Local tests do not certify live provider behavior or approve the production policy.