Technology Vision¶
A north star for how platform technologies fit together and where we invest.
📋 Overview¶
Forge uses multiple technologies that overlap in capability — TypeSpec and Aspire can both generate code, Terraform and Pulumi can both provision infrastructure, Azure DevOps and Aspire can both orchestrate deployments. This document defines the role each technology plays and the boundaries between them.
Use this document to answer:
- "Where does this technology fit in the platform?"
- "Should we use the vendor default or build our own?"
- "Is this new tool aligned with our direction?"
This is NOT:
- A migration plan with timelines or milestones
- A mandate to rewrite existing systems
- A replacement for the Principles — it builds on them
🔗 See Also: Principles for the decision-making framework these choices are evaluated against.
🏗️ The Layered Model¶
The platform is composed of four layers plus a cross-cutting observability concern. Each layer has a clear responsibility and a clear boundary. The CLI sits above as the unified developer surface.
┌─────────────────────────────────────────────────┐
│ Developer Surface (SAIF CLI) │ ← How developers interact
├─────────────────────────────────────────────────┤
│ Runner Layer (Azure DevOps / GitHub Actions) │ ← When and where to run
├─────────────────────────────────────────────────┤
│ Orchestration Layer (Aspire) │ ← How things connect & deploy
├─────────────────────────────────────────────────┤
│ Contract Layer (TypeSpec) │ ← What the API looks like
└─────────────────────────────────────────────────┘
Observability (OpenTelemetry) ← Universal signal layer
Defaults First, Extend When Needed¶
Each layer relies on an extensible framework provided by a vendor. We use vendor defaults wherever they meet our requirements. We extend at the framework's designated extension points only when the default can't meet security, compliance, or operational needs.
| Layer | Framework (Vendor) | Default Works? | SAIF Extension |
|---|---|---|---|
| Contract | TypeSpec compiler | OpenAPI generation ✅ | Custom emitter for server code |
| Contract | TypeSpec compiler | C# server stubs ❌ | Custom emitter for production patterns |
| Orchestration | Aspire | Local orchestration ✅ | — |
| Orchestration | Aspire | Deployment targets ❌ | Custom hosting integrations |
| Infrastructure | Pulumi / Terraform | Engine ✅ | Custom modules for SAIF patterns |
| Observability | OpenTelemetry | ServiceDefaults ✅ | — |
| Runner | Azure DevOps | Pipeline execution ✅ | Minimal YAML, invokes C# logic |
The decision boundary is simple: "Can the default meet our security, compliance, and operational requirements?" If yes, use it. If no, extend at the framework's extension point. Never rebuild what the framework already does well.
📝 Contract Layer — TypeSpec¶
Role: Define the shape of APIs and data models using a purpose-built language.
What the Framework Provides¶
TypeSpec gives us a compiler, type system, decorators, and an emitter plugin architecture. It's designed to be extended — emitters are the primary way TypeSpec produces output for different targets.
What We Use As-Is¶
- OpenAPI generation — the
@typespec/openapi3emitter produces spec-compliant OpenAPI documents. It's excellent and needs no customization.
Where We Extend¶
- Server code generation — the
@typespec/http-server-csharpemitter is the natural default. Microsoft recently rewrote it on the Alloy framework, signalling renewed investment. Before building anything custom, we evaluate whether its output quality now meets the enterprise bar and whether it can be extended (via the TypeSpec Emitter Framework) to express SAIF conventions: authorization wiring, controller augmentation model, service layer contracts, Mapperly-based mappers, and telemetry integration. If the default emitter's extension points are sufficient, we layer SAIF patterns on top. If not, we build a custom emitter on the same Alloy foundation. - Client SDK generation — we use Kiota to generate clients from OpenAPI specs. The interchange format stays OpenAPI; TypeSpec never needs to be published or distributed to consumers. Kiota generates model classes per client namespace, so shared response shapes (notably error types) get duplicated once per generated client. We investigated this extensively — shared external
$refschemas, the@typespec/http-client-csharpemitter, custom generator plugins, and post-generation type unification all introduce more complexity than the problem warrants. No existing tool solves this cleanly, and building custom tooling around it is disproportionate effort. We accept the duplication as a cosmetic cost of generated code. For unified error handling, all Kiota-generated error types extendApiException— consumers catch the common base class and read.ResponseStatusCoderather than dealing with per-client concrete types.
Boundary¶
TypeSpec defines what the API looks like — endpoints, models, validation rules, authorization requirements. It does NOT define how services discover each other, where they deploy, or how they scale. That's Aspire's job.
🔌 Orchestration Layer — Aspire¶
Role: Define how services discover each other, what infrastructure they need, and how they deploy.
What the Framework Provides¶
Aspire gives us the AppHost model, service discovery, a pipeline system (aspire publish / aspire deploy), and hosting integration extension points. The AppHost is the single source of truth for the application model across local development and production.
What We Use As-Is¶
- Local orchestration — Aspire's AppHost, service discovery, and dashboard are proven and work perfectly for local development. No customization needed.
- Pipeline model — the
aspire publish/aspire deployentry points and step-based pipeline are used as designed.
Where We Extend¶
- Deployment targets — the built-in targets (Azure Container Apps, App Service) deploy with generic patterns. Enterprise deployments need private networking, managed identity wiring, compliance tagging, multi-environment promotion, state management, and team-specific RBAC. We build custom hosting integrations (NuGet packages) that contribute Terraform or Pulumi steps to the Aspire pipeline.
How the Pieces Fit¶
Aspire's deployment model explicitly separates two responsibilities:
- CI/CD pipeline (Azure DevOps) decides when jobs run, which approvals they require, and which credentials are available
- Aspire pipeline (AppHost) decides what the application means and which deployment steps are required
This separation means the CI/CD YAML stays thin — it invokes aspire deploy and lets the AppHost handle resource topology, ordering, and infrastructure composition.
Boundary¶
Aspire defines how things connect and deploy — service topology, infrastructure composition, deployment orchestration. It does NOT define API contracts, business logic, or data models. That's TypeSpec's job (contracts) and application code (logic).
Industry alignment
This layer maps to the Platform Orchestrator pattern described by Humanitec and the Integration & Delivery Plane in the platformengineering.org tooling landscape. Aspire's AppHost acts as the workload specification + context resolver that Platform Orchestrators provide.
🖥️ Developer Surface — SAIF CLI¶
Role: The single interface through which developers interact with the platform. In lieu of an internal developer portal, the CLI plays that role — discovery, scaffolding, environment management, and AI agent integration from a unified command surface.
What the CLI Provides¶
- Scaffolding (
saif new) — create projects from platform templates (Golden Path) - Environment health (
saif doctor) — diagnose and remediate toolchain issues - Discovery (
saif search,saif docs,saif app) — find repos, documentation, and app registrations - Pipeline operations (
saif pipeline) — trigger and monitor CI/CD builds - Auth management (
saif auth) — generate and validate OAuth tokens - AI agent integration (
saif agent) — expose platform capabilities to coding agents via MCP
Design Principles¶
- Shell-first — every agent has shell access; CLI commands are stateless, composable, and token-efficient
- Structured output (
--format json) — humans get tables, agents get JSON; same commands, different presentation - Same commands, any actor — AI agents use identical commands to humans; no separate "agent API"
- Defaults first — sensible defaults for interactive use, explicit flags for automation
Boundary¶
The CLI is a thin orchestration surface — it calls into platform layers (Aspire, TypeSpec, Azure DevOps) rather than reimplementing their logic. It doesn't own business rules, deployment topology, or API contracts. It connects developers to the layers that do.
🧩 Platform Language — C#¶
Thesis: The platform's own tooling is written in C# to reduce context-switching across platform components and enable shared infrastructure. Application code is developer's choice — we support C# and TypeScript.
What Lives in C#¶
- SAIF CLI and platform tooling
- Aspire hosting integrations and AppHost orchestration
- Custom TypeSpec emitters (server code generation)
- Infrastructure definitions (via Pulumi C# SDK)
- Build and deployment logic (invoked by thin pipeline runners)
What Stays Outside C#¶
- Application code — developers choose C# or TypeScript depending on their domain. The platform supports both.
- Frontend UI — TypeScript and React are the right tools for browser code.
- Pipeline runner execution — YAML defines when and where to run. Keeping it thin is the goal, not eliminating it.
🚀 Runner Layer — Pipelines¶
Role: Execute pre-defined logic — build, test, deploy.
North Star¶
Pipelines become a thin execution shell. Logic lives in C# and is invoked via CLI commands (dotnet, aspire, pulumi). YAML defines when and where to run, not what to do.
What This Means in Practice¶
- Pipeline templates shrink — they call
aspire deploy --environment productionrather than encoding resource topology in YAML steps - Build logic moves into the application (MSBuild targets, Aspire pipeline steps)
- Test orchestration is a
dotnet testinvocation, not a complex multi-step YAML job - Infrastructure provisioning is an Aspire pipeline step, not a separate Terraform stage with variable groups and manual approvals
Why¶
Heavy pipeline YAML is difficult to test locally, requires specialized knowledge, and drifts from the application model. When deployment logic lives in the AppHost, developers can run and debug it on their machines before pushing to CI.
📡 Observability — OpenTelemetry¶
Role: Universal, vendor-agnostic telemetry (traces, metrics, logs).
North Star¶
All services emit OpenTelemetry signals by default via Aspire ServiceDefaults. The Aspire dashboard provides local observability with full parity to production. The choice of observability vendor (Dynatrace today) is an infrastructure concern — applications emit signals, the platform routes them.
What This Means¶
- Applications never reference a vendor SDK directly
- Switching vendors requires infrastructure configuration changes, not code changes
- Local development has the same observability experience as production (traces, metrics, logs visible in the Aspire dashboard)
📊 Today vs. North Star¶
This table summarizes where we are and where we're heading. It's a direction, not a deadline.
| Concern | Today | North Star |
|---|---|---|
| Developer surface | SAIF CLI (scaffolding, search, doctor) | CLI as internal developer portal (discovery, agents, self-service) |
| API contracts | TypeSpec → OpenAPI + default C# stubs | TypeSpec → OpenAPI + custom emitter (production code) |
| Client generation | Kiota from OpenAPI | Kiota from OpenAPI (accepted; unified handling via ApiException base) |
| Local development | Aspire orchestration | Aspire orchestration (unchanged — proven) |
| Infrastructure | Terraform (HCL) modules | C# via Pulumi, contributed as Aspire pipeline steps |
| Deployment | Azure DevOps pipeline templates (heavy YAML) | aspire deploy + custom hosting integrations + thin runner |
| Observability | OpenTelemetry + Dynatrace | OpenTelemetry + Dynatrace (unchanged, vendor-swappable) |
| Build logic | Pipeline YAML steps | C# tasks invoked by thin runner |
🔑 Principle Alignment¶
This vision is evaluated against the same Principles that guide all platform decisions:
| Principle | How This Vision Aligns |
|---|---|
| Zero-Trust | Custom emitter bakes in authorization; custom hosting integrations enforce private networking and managed identities |
| Proven Patterns | "Defaults first" is opinionated defaults; custom emitter generates code with patterns baked in |
| Leverage Over Invention | Using frameworks' designed extension points because defaults aren't fit-for-purpose — not reinventing the framework |
| Speed-to-Value | Custom emitter generates deployment-ready code; hosting integrations enable aspire deploy to production |
| Team Autonomy | Self-service deploy via Aspire; CLI as portal — no pipeline YAML expertise needed |
| Local-First Orchestration | Aspire local orchestration stays default — same model local and production |
| Platform as a Product | Layered model defines clear boundaries; CLI is the product surface |
📎 Industry Alignment¶
The layered model above is a developer-facing application architecture guide — it answers "how should I structure my app?" The industry platform engineering taxonomies (CNCF Platforms White Paper, platformengineering.org 5 Planes, internaldeveloperplatform.org Core Components) answer a different question: "what should the platform offer?" They're complementary, not competing.
| Our Layer | Industry Term | Source |
|---|---|---|
| Developer Surface (CLI) | Internal Developer Portal / Developer Control Plane | platformengineering.org 5 Planes, Backstage |
| Contract (TypeSpec) | No standard equivalent — closest is "API-first design" or "Workload Specification" | General practice |
| Orchestration (Aspire) | Platform Orchestrator / Infrastructure Orchestration + Environment Management | Humanitec, internaldeveloperplatform.org |
| Runner (Azure DevOps) | Integration & Delivery Plane | platformengineering.org 5 Planes |
| Observability (OTel) | Observability Plane | platformengineering.org 5 Planes, CNCF |