Skip to content

ProjectService

Target

This page describes intended behaviour or structure. No implementation is claimed unless a section is explicitly marked Current implementation or Evidence.

Bounded context BC-01 Project & Environment
Platform / plane Control Plane / Control
Deployment granularity module of host · host ControlPlaneApi
Status target · ConnectSoft.Factory.Projects
Built from connectsoft-microservice · libraries: connectsoft-extensions-saas-abstractions, connectsoft-extensions-saas-asp-net-core, connectsoft-extensions-messaging-model, connectsoft-extensions-persistence-model-nhibernate
Decisions ADR-0015, ADR-0019
Traceability FR-01 FUX-01 NFR-01 NFR-03 XL-01 XL-13

Purpose and responsibilities

A project is the workspace in which the Factory does work for a tenant. ProjectService owns the project itself, who may act in it and in which role, what sources a run in it may touch, and which policy tightening applies to it. It is the authority for the projectId that appears in every run, approval, usage record and artifact, and it resolves a subject's effective roles for every command the other modules accept.

It does not own identities or credentials (the canonical Identity Platform does, ADR-0101), tenants or editions (SaaS foundations, BC-33), environments (EnvironmentService), budgets (CostUsageService) or policy definitions (BC-18). It absorbs the June-2026 rows ModuleCatalogService and DependencyService: module and dependency metadata of a generated product is a Software Factory pack concern (BC-02 blueprints) and the fleet inventory (BC-35), not a Control plane aggregate.

Context and boundaries

flowchart LR
    S["ProjectService"]
    StudioBff["StudioBff"] -->|sync-http| S
    RunControlService["RunControlService"] -->|grpc| S
    S -->|sync-http| ext_identity_platform["ext-identity-platform"]
    S -->|grpc| PolicyEngineService["PolicyEngineService"]
    S -->|event| RunControlService["RunControlService"]
    S -->|grpc| CostUsageService["CostUsageService"]
    S -. "ProjectCreated" .-> E_ProjectCreated(("event"))
    S -. "ProjectStateChanged" .-> E_ProjectStateChanged(("event"))
    S -. "MembershipChanged" .-> E_MembershipChanged(("event"))
    S -. "DelegationChanged" .-> E_DelegationChanged(("event"))
    S -. "SourceScopeChanged" .-> E_SourceScopeChanged(("event"))
Hold "Alt" / "Option" to enable pan & zoom

Roles are the seven of ADR-0015 point 5 (D18). Six are project memberships (MembershipRole); Platform admin is tenant-wide and comes from the Identity Platform claims, never from a membership row. Separation of duties is enforced where the decision is made (Run Control, Asset Publication) using the roles this service resolves; this service only guarantees that the data needed for those checks is consistent.

A delegation (D18) lets a member hand the same or a lesser role to another subject for a bounded period with a recorded reason; it is revocable, expires automatically and is audited. It never grants Platform admin and never escalates.

Domain model

Project — A workspace inside a tenant with memberships, source scope, policy overrides and environments; the unit of budget, approval policy and data retention. (catalog entry)

Field Type Required Description
projectId guid yes
tenantId string yes
key string yes Short unique key within the tenant (used in URLs and labels).
name string yes
state enum:ProjectState yes
region string yes Inherited from the tenant; recorded for audit (ADR-0019).
policyOverrideRevision string no Project policy revision; may only tighten platform policy (D18).
retentionDays int yes Run and artifact retention (default 365; D31).
budgetId guid no Project budget in Usage Accounting (BC-05).
version long yes

Invariants:

  • Project keys are unique within a tenant and immutable after creation.
  • Memberships reference identities that exist in the Identity Service; the Factory never stores credentials.
  • A project policy override may only be stricter than the platform policy (validated by BC-18 at save).
  • Closing a project cancels its open runs, revokes delegations and starts export or deletion per tenant policy before Closed.
stateDiagram-v2
    Active --> Suspended : Suspend
    Suspended --> Active : Resume
    Active --> Closing : Close
    Suspended --> Closing : Close
    Closing --> Closed : OffboardingCompleted
Hold "Alt" / "Option" to enable pan & zoom

Events: ProjectCreated, ProjectStateChanged, MembershipChanged, DelegationChanged, SourceScopeChanged · Tables: Projects, Memberships, Delegations, SourceScopes · Repository: ProjectRepository

erDiagram
    Project {
        guid projectId PK
        string tenantId
        string key
        string name
        enum_ProjectState state
        string region
        string policyOverrideRevision
        int retentionDays
        guid budgetId
        long version
    }
    Membership {
        string subjectId

        enum_MembershipRole role

        string grantedBy

        datetime grantedAt

    }
    Delegation {
        guid delegationId

        string grantorId

        string granteeId

        enum_MembershipRole role

        datetime expiresAt

        string reason

        datetime revokedAt

    }
    SourceScopeEntry {
        string kind

        string reference

        guid connectionId

    }
    ProjectKey {
        string value

    }
    Project ||--o{ Membership : contains
    Project ||--o{ Delegation : contains
    Project ||--o{ SourceScopeEntry : contains
Hold "Alt" / "Option" to enable pan & zoom
stateDiagram-v2
    [*] --> Active: CreateProject
    Active --> Suspended: Suspend
    Suspended --> Active: Resume
    Active --> Closing: Close
    Suspended --> Closing: Close
    Closing --> Closed: OffboardingCompleted
    Closed --> [*]
Hold "Alt" / "Option" to enable pan & zoom

Closing is a real state because off-boarding (XL-13, XL-14) is a procedure with external effects: open runs are cancelled through Run Control, delegations are revoked, and the tenant's export or deletion choice is executed per the data governance rules before the project becomes Closed.

Entities and value objects

Membership (entity, in Project) — A subject's role assignment in the project.

Field Type Required Description
subjectId string yes
role enum:MembershipRole yes
grantedBy string yes
grantedAt datetime yes

Delegation (entity, in Project) — Time-bound delegation of the same or a lesser role (D18); audited and revocable.

Field Type Required Description
delegationId guid yes
grantorId string yes
granteeId string yes
role enum:MembershipRole yes
expiresAt datetime yes
reason string yes
revokedAt datetime no

SourceScopeEntry (entity, in Project) — A repository, document collection or tool a run in this project may touch.

Field Type Required Description
kind string yes repository
reference string yes
connectionId guid no Connection in BC-16 when credentials are needed.

ProjectKey (value-object) — Lower-case key 3–32 characters.

Field Type Required Description
value string yes

Data model and storage

Store Role Engine / profile Tables
Control plane relational store primary azure-sql / nhibernate-sqlserver Projects, Memberships, Delegations, SourceScopes
Table Owner Columns Indexes
dbo.Projects Project 11 1
dbo.Memberships Membership 6 0
dbo.Delegations Delegation 9 1
dbo.SourceScopes SourceScopeEntry 5 0

Full DDL: appendix.

Tables live in the projects schema of control-sql. Memberships is keyed by project, subject and role so that a subject may hold several roles; the Identity Platform remains the source of the subject's existence and display data, which this service never caches beyond the subject id.

APIs

Method Path Operation Scope Side effects Idempotency Gateway Summary
POST /factory/projects CreateProject factory.projects.write shared-write key-header GW-03 Create a project in the caller's tenant; the caller becomes Workspace admin.
GET /factory/projects ListProjects factory.projects.read read natural GW-03 Projects the caller is a member of.
GET /factory/projects/{projectId} GetProject factory.projects.read read natural GW-03 Project with memberships, delegations, source scope and environments.
PUT /factory/projects/{projectId}/members/{subjectId} SetMembership factory.projects.admin shared-write natural GW-03 Grant or change a project role (Workspace admin).
POST /factory/projects/{projectId}/delegations Delegate factory.projects.delegate shared-write key-header GW-03 Delegate the same or a lesser role for a bounded period with a reason (D18).
DELETE /factory/projects/{projectId}/delegations/{delegationId} RevokeDelegation factory.projects.delegate shared-write natural GW-03 Revoke a delegation immediately.
PUT /factory/projects/{projectId}/source-scope SetSourceScope factory.projects.admin shared-write natural GW-03 Replace the repositories, document collections and tools a run may touch.
POST /factory/projects/{projectId}/close CloseProject factory.projects.admin production natural GW-03 Start off-boarding (cancel runs, revoke delegations, export or delete per tenant policy).
gRPC service Method Streaming Visibility Side effects Summary
Projects ResolveScope none internal read Resolve a subject's roles, source scope and policy override for a project; used by every module at request time.

ResolveScope is the hot path: every module calls it (or reads its cached result for the request) to build the trusted execution-scope-v1 object from the authenticated subject, the project and the environment. Its result is cached per request only; cross-request caching would make a revoked delegation effective for longer than the audit says.

Messages

Event Topic Scope / v Aggregate Consumers Summary
ProjectCreated projects.projects.v1.project-created integration / 1 Project CostUsageService, StudioBff, AuditService A project exists with its first Workspace admin.
ProjectStateChanged projects.projects.v1.project-state-changed integration / 1 Project IntegrationService, ArtifactService, RunControlService, StudioBff, AuditService, KnowledgeIngestionService A project moved between Active, Suspended, Closing and Closed.
MembershipChanged projects.memberships.v1.membership-changed integration / 1 Project ApprovalPolicyService, StudioBff, AuditService A role was granted, changed or removed.
DelegationChanged projects.delegations.v1.delegation-changed integration / 1 Project ApprovalPolicyService, StudioBff, AuditService A delegation was granted, expired or revoked.
SourceScopeChanged projects.scope.v1.source-scope-changed integration / 1 Project KnowledgeIngestionService, RunControlService The set of sources a run may touch changed.

ProjectCreated payload:

Field Type Required Description
projectId guid yes
tenantId string yes
key string yes
region string yes

ProjectStateChanged payload:

Field Type Required Description
projectId guid yes
tenantId string yes
fromState enum:ProjectState yes
toState enum:ProjectState yes

MembershipChanged payload:

Field Type Required Description
projectId guid yes
tenantId string yes
subjectId string yes
role enum:MembershipRole yes
change string yes

DelegationChanged payload:

Field Type Required Description
delegationId guid yes
projectId guid yes
tenantId string yes
granteeId string yes
role enum:MembershipRole yes
change string yes

SourceScopeChanged payload:

Field Type Required Description
projectId guid yes
tenantId string yes
entries list:string yes
Command Handled by Result events Summary
CreateProject Project ProjectCreated
CloseProject Project ProjectStateChanged

None modelled

No consumed events are modelled for this item yet.

Workers and jobs

Worker Trigger Output events Retry Idempotency key Summary
DelegationExpiryReconciler schedule */10 * * * * DelegationChanged fixed delegationId + expiresAt Expires delegations past expiresAt and emits DelegationChanged.
ProjectOffboardingWorker event ProjectStateChanged exponential projectId + Closing Runs the Closing procedure: cancels open runs through Run Control, revokes delegations, triggers export or deletion (XL-13, XL-14) and completes to Closed.

Actors and grains

None modelled

No grains are modelled for this item yet.

Sagas and long-running flows

None modelled

No sagas are modelled for this item yet.

The off-boarding worker is a journaled procedure rather than a saga: each completed step is recorded on the project, so a restart resumes at the next step.

sequenceDiagram
    participant A as Workspace admin
    participant P as ProjectService
    participant RC as RunControlService
    participant K as KnowledgeIngestionService
    participant AR as ArtifactService
    A->>P: POST /factory/projects/{id}/close
    P->>P: Active to Closing, commit with outbox
    P-)RC: ProjectStateChanged
    RC->>RC: cancel open runs of the project
    P->>P: revoke delegations
    P-)K: ProjectStateChanged, indexes scheduled for deletion
    P->>AR: export or delete per tenant policy
    P->>P: Closing to Closed when every step is journaled
Hold "Alt" / "Option" to enable pan & zoom

Options and configuration

Section Key Type Default Reloadable Secret Summary
Projects:Retention DefaultRetentionDays int 365 yes no Default run and artifact retention for new projects (D31).
Projects:Delegation MaxDelegationDays int 30 yes no Longest delegation period.

Metrics and observability

Metric Instrument Unit Dimensions Summary
connectsoft.factory.projects.active updown {project} tenant Active projects.
connectsoft.factory.projects.delegations_active updown {delegation} tenant Unexpired delegations.

Security

Authentication oauth2-jwt
Scopes factory.projects.read (Read projects.); factory.projects.write (Create projects.); factory.projects.admin (Memberships, scope, close (Workspace admin).); factory.projects.delegate (Delegate roles.)
Roles Requester, Reviewer, AssetMaintainer, Operator, WorkspaceAdmin, PlatformAdmin, Observer
Tenant isolation row-filter
Data classification internal
Max side-effect class production
Secrets —

Dependencies

Outbound

Target Kind Purpose Failure mode Required
ext-identity-platform sync-http Validate that membership subjects exist; read tenant memberships. fail-fast yes
PolicyEngineService grpc Validate that a project policy override only tightens platform policy. fail-fast yes
RunControlService event Cancel open runs on close. queue yes
CostUsageService grpc Create the project budget. degrade no

Inbound

Target Kind Purpose Failure mode Required
StudioBff sync-http Project administration and membership views. — yes
RunControlService grpc ResolveScope at submission and decision time. — yes

Failure and recovery

Scenario Detection Recovery Data impact RTO (min)
Identity Platform unavailable during membership change subject lookup fails command rejected with 503; no partial membership none 15
Off-boarding interrupted project stays Closing beyond threshold worker resumes idempotently from the journal of completed steps none 60

NFR targets

Target Class value (pilot-interactive) Override
latency_p95_ms 2000 —
availability_pct 99.5 —
throughput_rps 50 —
rpo_minutes 15 —
rto_minutes 240 —
cost_class low —

Deployment

Granularity module of host · host ControlPlaneApi
Topologies saas-default, hybrid, on-prem
Runtime —; replicas —–—
Ports —

Current implementation

Nothing exists. The tenancy primitives it builds on exist (ConnectSoft.Extensions.Saas.*, canonical ADR-0100) and are not bound into any Factory execution.

Later capabilities

Project templates (pre-set memberships, environments and source scope for a vertical); cross-project source sharing with explicit grants; organization-level groups mapped to memberships through the Identity Platform.

Decisions and traceability

Family Ids
FR FR-01
FUX FUX-01
NFR NFR-01, NFR-03
XL XL-01, XL-13