Specifications as the Source of Truth in plain
Introduction
plain was built on a simple but strict idea: software should be built from specifications, and the process of building it should be deterministically repeatable. Given the same set of specifications, plain should always generate the same software. This is a deliberate departure from prompt-driven or ad hoc generation, where structure is implicit and outcomes vary from run to run.
In plain, specifications are the source of truth. They are the artifacts that are written, reviewed, and committed to the source-control repository. Code is a generated artifact that is deployed to a runtime, but can always be regenerated from the underlying specifications. This separation allows systems to evolve without drifting away from their original intent.
plain organizes specifications into a small number of concrete buckets:
- How: generator configuration that constrains generation-time decisions and architectural choices.
- What: application-level specifications that define data models, relationships, screens, workflows, AI agents etc.
- Where: infrastructure configuration that defines the runtime and deployment environment.
This post walks through these specifications, showing how generator configuration, application specifications, and infrastructure intent work together to produce production-ready systems from declarative inputs.
Generator Configuration
Every component factory in plain takes a generator configuration as input. The generator config controls how a component is generated. It defines generation-time behavior and constraints, not application logic or runtime behavior. This separation is intentional. The generator config exists to produce code in a predictable and repeatable way.
Generator configuration follows a consistent contract across all component factories in plain. Each generator accepts a json-based config that is validated against a strict schema before generation begins. Invalid or incomplete configs fail early. The generator does not attempt to infer missing intent or silently apply best-effort assumptions. This ensures that the same inputs always produce the same outputs.
All component factories ship with reasonable defaults. Builders can get started with a minimal generator config and still generate a complete, production-ready module. Defaults encode opinionated but safe choices around structure, layout, and conventions. When customization is required, it is done explicitly through the generator config rather than through post-generation edits.
Generator configuration typically includes information required by the generator to produce a complete module, such as:
- Output directories and file layout
- Logging and diagnostics configuration
- Language or framework selection where applicable
- AI model selection for generators that use AI
- Any other generation-time settings required by the component factory
These settings are inputs to the generator itself. They are not part of the application’s domain model and are not shipped with the generated service. Once code is generated, the generator config has served its purpose and can always be reused to regenerate the same module.
While the exact shape of the generator config varies by component, the pattern remains consistent. Below is an example from the plain-services-crud component, shown for illustration only:
Generator Config
In this example, the config specifies where the generated service should be written, the api ingress host etc. The config does not describe the data model, routes, or business logic of the service. Those concerns are handled by application-level specifications, which are covered next.
Data and Application Specifications
Data and application specifications represent the application domain and the business knowledge of a system. They describe what the system is responsible for, independent of how code is generated or where it runs. These specifications are the primary inputs that define the shape, behavior, and responsibilities of an application in plain.
While the exact format varies by component, the underlying approach is consistent. The specification always describes intent in a declarative way and is validated against a strict schema. Depending on the component, the same idea may surface as a data model, a user-facing screen, or an AI agent service. Different surfaces, same principle.
Data Model as Specification
At the data layer, plain uses dbml to define entities and their relationships. The dbml specification acts as the canonical representation of the application’s data model. From this single source, plain generates persistence logic, APIs, and data access layers in a consistent way.
In addition to standard dbml constructs, plain supports a small number of plain-specific annotations. These can be used to indicate requirements such as authentication or access constraints that should be reflected in the generated application. The intent remains explicit and centralized in the specification rather than scattered across handwritten code.
DBML Schema
The specification shown above illustrates how entities and relationships are declared clearly and readably, making the data model easy to review and evolve as business requirements change.
Application Behavior as Specification
Beyond data, plain uses application specifications to describe how users and systems interact with the application. These specifications focus on behavior and structure rather than implementation details.
In plain-frontend-web, screens are defined using declarative yaml specifications. A screen specification describes what data the screen depends on, how it is structured, and what actions it supports. The builder focuses on the business model and user flow, while rendering, data wiring, and state management are handled by the generated code.
App Screen
The specification above shows an example of a screen specification. Even without understanding every field, it is clear what the screen represents and how it fits into the application. The spec communicates intent directly, without requiring the reader to reason about framework-specific implementation details.
Similarly, in plain-ai-agents, agent services are defined using declarative yaml specifications. An agent specification describes the responsibility of the agent, the inputs it accepts, and the tools it is allowed to use. The agent is treated as a structured service rather than a free-form prompt.
Agent Definition
The example shown above demonstrates how an agent can be described declaratively, with clear boundaries and responsibilities. This makes agent behavior reviewable, testable, and easier to integrate into larger systems.
Validation and Evolution
All data and application specifications in plain are validated against schemas before generation. As business requirements change, builders update the specifications, not the generated code. The system is then regenerated from the updated specs, keeping implementation aligned with intent.
This approach allows applications to evolve without accumulating drift, while keeping domain knowledge centralized, explicit, and reviewable.
Infrastructure specifications
Infrastructure specifications define what a component needs in order to run in production. They describe requirements such as compute, data stores, secrets management, and other runtime dependencies. These specifications are declarative and scoped to the component. They do not describe how infrastructure is provisioned or managed. Like other specifications in plain, infrastructure requirements are explicit, validated, and versioned alongside application intent.
Every component in plain includes an infrastructure specification written in yaml. The structure is consistent across components and validated against a schema. This ensures that infrastructure requirements are explicit, reviewable, and versioned alongside the rest of the system’s specifications.
The focus of the infrastructure specification is on intent rather than implementation. A component declares that it requires a certain class of compute, access to a data store, or integration with a secrets manager. It does not specify cloud providers, resource identifiers, or provisioning steps. Those concerns are intentionally kept out of the component definition.
Infrastructure Specification
Provisioning and deployment are handled separately by the plain-infra-provisioning module. This module consumes infrastructure specifications and generates the deployment manifests required to create infrastructure and deploy services in a given environment. By separating declaration from provisioning, the same component can be deployed consistently across environments without changing the application or infrastructure specs.
The specifications above illustrate how infrastructure requirements are expressed clearly and concisely. Even at a glance, it is apparent what a component needs in order to run, without exposing environment-specific details. This keeps infrastructure intent centralized, portable, and aligned with the rest of plain’s specification-driven approach.
From Specification to Production
In plain, teams define generator configuration, data models, application specifications, and infrastructure requirements for the components they choose to use. These specifications are written once, validated, and committed as the source of truth for the system. They describe intent across generation, application behavior, and runtime requirements in a consistent and explicit way.
An orchestrator is responsible for coordinating the generation process. It invokes the relevant component generators in dependency order, producing complete, self-contained modules. The resulting services are ready to be deployed into any environment and on any cloud, without manual wiring or post-generation modification.
As requirements evolve, teams update the specifications rather than editing generated code. The system is regenerated from the updated inputs, ensuring that implementation remains aligned with intent. This makes change predictable and reversible, even as systems grow in complexity.
plain does not require teams to adopt the entire platform at once. Individual components can be used independently. For example, a team may choose to generate only an agent service using plain-ai-agents and integrate it into an existing frontend or workflow through a service endpoint. This allows teams to adopt plain incrementally while retaining control over their broader architecture.
