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"))
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
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
stateDiagram-v2
[*] --> Active: CreateProject
Active --> Suspended: Suspend
Suspended --> Active: Resume
Active --> Closing: Close
Suspended --> Closing: Close
Closing --> Closed: OffboardingCompleted
Closed --> [*]
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
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 |